Merge branch 'develop' into skopt

This commit is contained in:
Janne Sinivirta
2018-07-02 11:27:27 +03:00
29 changed files with 1485 additions and 424 deletions

View File

@@ -1,5 +1,5 @@
""" FreqTrade bot """
__version__ = '0.17.0'
__version__ = '0.17.1'
class DependencyException(BaseException):

View File

@@ -98,6 +98,13 @@ class Analyze(object):
"""
return self.strategy.ticker_interval
def get_stoploss(self) -> float:
"""
Return stoploss to use
:return: Strategy stoploss value to use
"""
return self.strategy.stoploss
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
@@ -172,33 +179,79 @@ class Analyze(object):
if the threshold is reached and updates the trade record.
:return: True if trade should be sold, False otherwise
"""
current_profit = trade.calc_profit_percent(rate)
if self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date):
return True
experimental = self.config.get('experimental', {})
if buy and experimental.get('ignore_roi_if_buy_signal', False):
logger.debug('Buy signal still active - not selling.')
return False
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
if self.min_roi_reached(trade=trade, current_rate=rate, current_time=date):
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
logger.debug('Required profit reached. Selling..')
return True
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
if self.config.get('experimental', {}).get('sell_profit_only', False):
if experimental.get('sell_profit_only', False):
logger.debug('Checking if trade is profitable..')
if trade.calc_profit(rate=rate) <= 0:
return False
if sell and not buy and self.config.get('experimental', {}).get('use_sell_signal', False):
if sell and not buy and experimental.get('use_sell_signal', False):
logger.debug('Sell signal received. Selling..')
return True
return False
def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool:
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime) -> bool:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
"""
current_profit = trade.calc_profit_percent(current_rate)
trailing_stop = self.config.get('trailing_stop', False)
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
# evaluate if the stoploss was hit
if self.strategy.stoploss is not None and trade.stop_loss >= current_rate:
if trailing_stop:
logger.debug(
f"HIT STOP: current price at {current_rate:.6f}, "
f"stop loss is {trade.stop_loss:.6f}, "
f"initial stop loss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}")
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")
logger.debug('Stop loss hit.')
return True
# update the stop loss afterwards, after all by definition it's supposed to be hanging
if trailing_stop:
# check if we have a special stop loss for positive condition
# and if profit is positive
stop_loss_value = self.strategy.stoploss
if 'trailing_stop_positive' in self.config and current_profit > 0:
# Ignore mypy error check in configuration that this is a float
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
f"since we have profit {current_profit}")
trade.adjust_stop_loss(current_rate, stop_loss_value)
return False
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should
sell
:return True if bot should sell at current rate
"""
current_profit = trade.calc_profit_percent(current_rate)
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
logger.debug('Stop loss hit.')
return True
# Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60

View File

@@ -262,17 +262,15 @@ class Arguments(object):
stop: int = 0
if stype[0]:
starts = rvals[index]
if stype[0] == 'date':
start = int(starts) if len(starts) == 10 \
else arrow.get(starts, 'YYYYMMDD').timestamp
if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp
else:
start = int(starts)
index += 1
if stype[1]:
stops = rvals[index]
if stype[1] == 'date':
stop = int(stops) if len(stops) == 10 \
else arrow.get(stops, 'YYYYMMDD').timestamp
if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp
else:
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)
@@ -336,3 +334,10 @@ class Arguments(object):
nargs='+',
dest='timeframes',
)
self.parser.add_argument(
'--erase',
help='Clean all existing data for the selected exchange/pairs/timeframes',
dest='erase',
action='store_true'
)

View File

@@ -11,6 +11,8 @@ RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
UNLIMITED_STAKE_AMOUNT = 'unlimited'
TICKER_INTERVAL_MINUTES = {
'1m': 1,
@@ -44,7 +46,11 @@ CONF_SCHEMA = {
'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': {'type': 'number', 'minimum': 0.0005},
'stake_amount': {
"type": ["number", "string"],
"minimum": 0.0005,
"pattern": UNLIMITED_STAKE_AMOUNT
},
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
@@ -55,7 +61,15 @@ CONF_SCHEMA = {
'minProperties': 1
},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
'trailing_stop': {'type': 'boolean'},
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'unfilledtimeout': {
'type': 'object',
'properties': {
'buy': {'type': 'number', 'minimum': 3},
'sell': {'type': 'number', 'minimum': 10}
}
},
'bid_strategy': {
'type': 'object',
'properties': {
@@ -73,7 +87,8 @@ CONF_SCHEMA = {
'type': 'object',
'properties': {
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}
'sell_profit_only': {'type': 'boolean'},
"ignore_roi_if_buy_signal_true": {'type': 'boolean'}
}
},
'telegram': {

View File

@@ -160,7 +160,7 @@ class FreqtradeBot(object):
if 'unfilledtimeout' in self.config:
# Check and handle any timed out open orders
self.check_handle_timedout(self.config['unfilledtimeout'])
self.check_handle_timedout()
Trade.session.flush()
except TemporaryError as error:
@@ -244,14 +244,69 @@ class FreqtradeBot(object):
balance = self.config['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def _get_trade_stake_amount(self) -> Optional[float]:
stake_amount = self.config['stake_amount']
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all())
if open_trades >= self.config['max_open_trades']:
logger.warning('Can\'t open a new trade: max number of trades is reached')
return None
return avaliable_amount / (self.config['max_open_trades'] - open_trades)
# Check if stake_amount is fulfilled
if avaliable_amount < stake_amount:
raise DependencyException(
'Available balance(%f %s) is lower than stake amount(%f %s)' % (
avaliable_amount, self.config['stake_currency'],
stake_amount, self.config['stake_currency'])
)
return stake_amount
def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]:
markets = self.exchange.get_markets()
markets = [m for m in markets if m['symbol'] == pair]
if not markets:
raise ValueError(f'Can\'t get market information for symbol {pair}')
market = markets[0]
if 'limits' not in market:
return None
min_stake_amounts = []
limits = market['limits']
if ('cost' in limits and 'min' in limits['cost']
and limits['cost']['min'] is not None):
min_stake_amounts.append(limits['cost']['min'])
if ('amount' in limits and 'min' in limits['amount']
and limits['amount']['min'] is not None):
min_stake_amounts.append(limits['amount']['min'] * price)
if not min_stake_amounts:
return None
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
if self.analyze.get_stoploss() is not None:
amount_reserve_percent += self.analyze.get_stoploss()
# it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5)
return min(min_stake_amounts)/amount_reserve_percent
def create_trade(self) -> bool:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:return: True if a trade object has been created and persisted, False otherwise
"""
stake_amount = self.config['stake_amount']
interval = self.analyze.get_ticker_interval()
stake_amount = self._get_trade_stake_amount()
if not stake_amount:
return False
stake_currency = self.config['stake_currency']
fiat_currency = self.config['fiat_display_currency']
exc_name = self.exchange.name
@@ -261,10 +316,6 @@ class FreqtradeBot(object):
stake_amount
)
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if self.exchange.get_balance(stake_currency) < stake_amount:
raise DependencyException(
f'stake amount is not fulfilled (currency={stake_currency})')
# Remove currently opened and latest pairs from whitelist
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
@@ -285,8 +336,18 @@ class FreqtradeBot(object):
return False
pair_s = pair.replace('_', '/')
pair_url = self.exchange.get_pair_detail_url(pair)
# Calculate amount
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning(
f'Can\'t open a new trade for {pair_s}: stake amount'
f' is too small ({stake_amount} < {min_stake_amount})'
)
return False
amount = stake_amount / buy_limit
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
@@ -423,8 +484,8 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
current_rate = self.exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False)
if self.config.get('experimental', {}).get('use_sell_signal'):
experimental = self.config.get('experimental', {})
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
(buy, sell) = self.analyze.get_signal(self.exchange,
trade.pair, self.analyze.get_ticker_interval())
@@ -434,13 +495,16 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
return False
def check_handle_timedout(self, timeoutvalue: int) -> None:
def check_handle_timedout(self) -> None:
"""
Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out
:return: None
"""
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
buy_timeout = self.config['unfilledtimeout']['buy']
sell_timeout = self.config['unfilledtimeout']['sell']
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try:
@@ -463,10 +527,12 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
if int(order['remaining']) == 0:
continue
if order['side'] == 'buy' and ordertime < timeoutthreashold:
self.handle_timedout_limit_buy(trade, order)
elif order['side'] == 'sell' and ordertime < timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
# Check if trade is still actually open
if order['status'] == 'open':
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
self.handle_timedout_limit_buy(trade, order)
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
# FIX: 20180110, why is cancel.order unconditionally here, whereas
# it is conditionally called in the

View File

@@ -14,6 +14,7 @@ from pandas import DataFrame
from tabulate import tabulate
import freqtrade.optimize as optimize
from freqtrade import constants, DependencyException
from freqtrade.exchange import Exchange
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments
@@ -37,6 +38,8 @@ class BacktestResult(NamedTuple):
close_index: int
trade_duration: float
open_at_end: bool
open_rate: float
close_rate: float
class Backtesting(object):
@@ -115,11 +118,10 @@ class Backtesting(object):
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
records = [(trade_entry.pair, trade_entry.profit_percent,
trade_entry.open_time.timestamp(),
trade_entry.close_time.timestamp(),
trade_entry.open_index - 1, trade_entry.trade_duration)
for index, trade_entry in results.iterrows()]
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end)
for index, t in results.iterrows()]
if records:
logger.info('Dumping backtest results to %s', recordfilename)
@@ -158,7 +160,9 @@ class Backtesting(object):
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index,
close_index=sell_row.Index,
open_at_end=False
open_at_end=False,
open_rate=buy_row.close,
close_rate=sell_row.close
)
if partial_ticker:
# no sell condition found - trade stil open at end of backtest period
@@ -171,7 +175,9 @@ class Backtesting(object):
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index,
close_index=sell_row.Index,
open_at_end=True
open_at_end=True,
open_rate=buy_row.close,
close_rate=sell_row.close
)
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
btr.profit_percent, btr.profit_abs)
@@ -341,6 +347,10 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]:
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT:
raise DependencyException('stake amount could not be "%s" for backtesting' %
constants.UNLIMITED_STAKE_AMOUNT)
return config

View File

@@ -21,7 +21,6 @@ from freqtrade import OperationalException
logger = logging.getLogger(__name__)
_CONF = {}
_DECL_BASE: Any = declarative_base()
@@ -33,9 +32,7 @@ def init(config: Dict) -> None:
:param config: config to use
:return: None
"""
_CONF.update(config)
db_url = _CONF.get('db_url', None)
db_url = config.get('db_url', None)
kwargs = {}
# Take care of thread ownership if in-memory db
@@ -61,7 +58,7 @@ def init(config: Dict) -> None:
check_migrate(engine)
# Clean dry_run DB if the db is not in-memory
if _CONF.get('dry_run', False) and db_url != 'sqlite://':
if config.get('dry_run', False) and db_url != 'sqlite://':
clean_dry_run_db()
@@ -69,6 +66,10 @@ def has_column(columns, searchname: str) -> bool:
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
def get_column_def(columns, column: str, default: str) -> str:
return default if not has_column(columns, column) else column
def check_migrate(engine) -> None:
"""
Checks if migration is necessary and migrates if necessary
@@ -76,18 +77,32 @@ def check_migrate(engine) -> None:
inspector = inspect(engine)
cols = inspector.get_columns('trades')
tabs = inspector.get_table_names()
table_back_name = 'trades_bak'
for i, table_back_name in enumerate(tabs):
table_back_name = f'trades_bak{i}'
logger.info(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'max_rate'):
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
max_rate = get_column_def(cols, 'max_rate', '0.0')
if not has_column(cols, 'fee_open'):
# Schema migration necessary
engine.execute("alter table trades rename to trades_bak")
engine.execute(f"alter table trades rename to {table_back_name}")
# let SQLAlchemy create the schema as required
_DECL_BASE.metadata.create_all(engine)
# Copy data back - following the correct schema
engine.execute("""insert into trades
engine.execute(f"""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id)
stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, initial_stop_loss, max_rate
)
select id, lower(exchange),
case
when instr(pair, '_') != 0 then
@@ -97,21 +112,18 @@ def check_migrate(engine) -> None:
end
pair,
is_open, fee fee_open, fee fee_close,
open_rate, null open_rate_requested, close_rate,
null close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id
from trades_bak
open_rate, {open_rate_requested} open_rate_requested, close_rate,
{close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
{max_rate} max_rate
from {table_back_name}
""")
# Reread columns - the above recreated the table!
inspector = inspect(engine)
cols = inspector.get_columns('trades')
if not has_column(cols, 'open_rate_requested'):
engine.execute("alter table trades add open_rate_requested float")
if not has_column(cols, 'close_rate_requested'):
engine.execute("alter table trades add close_rate_requested float")
def cleanup() -> None:
"""
@@ -154,6 +166,12 @@ class Trade(_DECL_BASE):
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
def __repr__(self):
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
@@ -164,6 +182,45 @@ class Trade(_DECL_BASE):
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
)
def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
"""this adjusts the stop loss to it's most recently observed setting"""
if initial and not (self.stop_loss is None or self.stop_loss == 0):
# Don't modify if called with initial and nothing to do
return
new_loss = float(current_price * (1 - abs(stoploss)))
# keeping track of the highest observed rate for this trade
if self.max_rate is None:
self.max_rate = current_price
else:
if current_price > self.max_rate:
self.max_rate = current_price
# no stop loss assigned yet
if not self.stop_loss:
logger.debug("assigning new stop loss")
self.stop_loss = new_loss
self.initial_stop_loss = new_loss
# evaluate if the stop loss needs to be updated
else:
if new_loss > self.stop_loss: # stop losses only walk up, never down!
self.stop_loss = new_loss
logger.debug("adjusted stop loss")
else:
logger.debug("keeping current stop loss")
logger.debug(
f"{self.pair} - current price {current_price:.8f}, "
f"bought at {self.open_rate:.8f} and calculated "
f"stop loss is at: {self.initial_stop_loss:.8f} initial "
f"stop at {self.stop_loss:.8f}. "
f"trailing stop loss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} "
f"and max observed rate was {self.max_rate:.8f}")
def update(self, order: Dict) -> None:
"""
Updates this entity with amount and actual open/close rates.

View File

@@ -72,7 +72,7 @@ class StrategyResolver(object):
"""
current_path = os.path.dirname(os.path.realpath(__file__))
abs_paths = [
os.path.join(current_path, '..', '..', 'user_data', 'strategies'),
os.path.join(os.getcwd(), 'user_data', 'strategies'),
current_path,
]
@@ -81,10 +81,13 @@ class StrategyResolver(object):
abs_paths.insert(0, extra_dir)
for path in abs_paths:
strategy = self._search_strategy(path, strategy_name)
if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
return import_strategy(strategy)
try:
strategy = self._search_strategy(path, strategy_name)
if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
return import_strategy(strategy)
except FileNotFoundError:
logger.warning('Path "%s" does not exist', path)
raise ImportError(
"Impossible to load Strategy '{}'. This class does not exist"

View File

@@ -100,7 +100,10 @@ def default_conf():
"0": 0.04
},
"stoploss": -0.10,
"unfilledtimeout": 600,
"unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": {
"ask_last_balance": 0.0
},
@@ -189,7 +192,10 @@ def markets():
'max': 1000,
},
'price': 500000,
'cost': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
@@ -211,7 +217,10 @@ def markets():
'max': 1000,
},
'price': 500000,
'cost': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
@@ -233,7 +242,85 @@ def markets():
'max': 1000,
},
'price': 500000,
'cost': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'ltcbtc',
'symbol': 'LTC/BTC',
'base': 'LTC',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'xrpbtc',
'symbol': 'XRP/BTC',
'base': 'XRP',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'neobtc',
'symbol': 'NEO/BTC',
'base': 'NEO',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
}

View File

@@ -14,13 +14,29 @@ from freqtrade.exchange import Exchange, API_RETRY_COUNT
from freqtrade.tests.conftest import log_has, get_patched_exchange
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
"""Function to test ccxt exception handling """
with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
def test_init(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
get_patched_exchange(mocker, default_conf)
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
def test_init_exception(default_conf):
def test_init_exception(default_conf, mocker):
default_conf['exchange']['name'] = 'wrong_exchange_name'
with pytest.raises(
@@ -28,6 +44,13 @@ def test_init_exception(default_conf):
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
Exchange(default_conf)
default_conf['exchange']['name'] = 'binance'
with pytest.raises(
OperationalException,
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError))
Exchange(default_conf)
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
@@ -97,6 +120,20 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
Exchange(conf)
def test_exchangehas(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf)
assert not exchange.exchange_has('ASDFASDF')
api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={'deadbeef': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.exchange_has("deadbeef")
type(api_mock).has = PropertyMock(return_value={'deadbeef': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert not exchange.exchange_has("deadbeef")
def test_buy_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf)
@@ -216,6 +253,11 @@ def test_get_balance_prod(default_conf, mocker):
exchange.get_balance(currency='BTC')
with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'):
exchange = get_patched_exchange(mocker, default_conf, api_mock)
mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={}))
exchange.get_balance(currency='BTC')
def test_get_balances_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
@@ -243,17 +285,8 @@ def test_get_balances_prod(default_conf, mocker):
assert exchange.get_balances()['1ST']['total'] == 10.0
assert exchange.get_balances()['1ST']['used'] == 0.0
with pytest.raises(TemporaryError):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_balances()
assert api_mock.fetch_balance.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_balances()
assert api_mock.fetch_balance.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_balances", "fetch_balance")
def test_get_tickers(default_conf, mocker):
@@ -282,15 +315,8 @@ def test_get_tickers(default_conf, mocker):
assert tickers['BCH/BTC']['bid'] == 0.6
assert tickers['BCH/BTC']['ask'] == 0.5
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
with pytest.raises(OperationalException):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_tickers", "fetch_tickers")
with pytest.raises(OperationalException):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
@@ -345,15 +371,9 @@ def test_get_ticker(default_conf, mocker):
exchange.get_ticker(pair='ETH/BTC', refresh=False)
assert api_mock.fetch_ticker.call_count == 0
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_ticker(pair='ETH/BTC', refresh=True)
with pytest.raises(OperationalException):
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_ticker(pair='ETH/BTC', refresh=True)
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_ticker", "fetch_ticker",
pair='ETH/BTC', refresh=True)
api_mock.fetch_ticker = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
@@ -416,17 +436,14 @@ def test_get_ticker_history(default_conf, mocker):
assert ticks[0][4] == 9
assert ticks[0][5] == 10
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# new symbol to get around cache
exchange.get_ticker_history('ABCD/BTC', default_conf['ticker_interval'])
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_ticker_history", "fetch_ohlcv",
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
with pytest.raises(OperationalException):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# new symbol to get around cache
exchange.get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
exchange.get_ticker_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
def test_get_ticker_history_sort(default_conf, mocker):
@@ -510,30 +527,20 @@ def test_cancel_order_dry_run(default_conf, mocker):
# Ensure that if not dry_run, we should call API
def test_cancel_order(default_conf, mocker):
default_conf['dry_run'] = False
# mocker.patch.dict('freqtrade.exchange.._CONF', default_conf)
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(TemporaryError):
api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(DependencyException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"cancel_order", "cancel_order",
order_id='_', pair='TKN/BTC')
def test_get_order(default_conf, mocker):
@@ -551,23 +558,15 @@ def test_get_order(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_order('X', 'TKN/BTC') == 456
with pytest.raises(TemporaryError):
api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(DependencyException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_order', 'fetch_order',
order_id='_', pair='TKN/BTC')
def test_name(default_conf, mocker):
@@ -652,19 +651,12 @@ def test_get_trades_for_order(default_conf, mocker):
assert len(orders) == 1
assert orders[0]['price'] == 165
# test Exceptions
with pytest.raises(OperationalException):
api_mock = MagicMock()
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_trades_for_order', 'fetch_my_trades',
order_id=order_id, pair='LTC/BTC', since=since)
with pytest.raises(TemporaryError):
api_mock = MagicMock()
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
assert api_mock.fetch_my_trades.call_count == API_RETRY_COUNT + 1
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == []
def test_get_markets(default_conf, mocker, markets):
@@ -673,24 +665,13 @@ def test_get_markets(default_conf, mocker, markets):
exchange = get_patched_exchange(mocker, default_conf, api_mock)
ret = exchange.get_markets()
assert isinstance(ret, list)
assert len(ret) == 3
assert len(ret) == 6
assert ret[0]["id"] == "ethbtc"
assert ret[0]["symbol"] == "ETH/BTC"
# test Exceptions
with pytest.raises(OperationalException):
api_mock = MagicMock()
api_mock.fetch_markets = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_markets()
with pytest.raises(TemporaryError):
api_mock = MagicMock()
api_mock.fetch_markets = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_markets()
assert api_mock.fetch_markets.call_count == API_RETRY_COUNT + 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_markets', 'fetch_markets')
def test_get_fee(default_conf, mocker):
@@ -705,19 +686,8 @@ def test_get_fee(default_conf, mocker):
assert exchange.get_fee() == 0.025
# test Exceptions
with pytest.raises(OperationalException):
api_mock = MagicMock()
api_mock.calculate_fee = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_fee()
with pytest.raises(TemporaryError):
api_mock = MagicMock()
api_mock.calculate_fee = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_fee()
assert api_mock.calculate_fee.call_count == API_RETRY_COUNT + 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_fee', 'calculate_fee')
def test_get_amount_lots(default_conf, mocker):

View File

@@ -3,6 +3,7 @@
import json
import math
import random
import pytest
from copy import deepcopy
from typing import List
from unittest.mock import MagicMock
@@ -11,7 +12,7 @@ import numpy as np
import pandas as pd
from arrow import Arrow
from freqtrade import optimize
from freqtrade import optimize, constants, DependencyException
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments, TimeRange
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
@@ -268,6 +269,28 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
)
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
conf = deepcopy(default_conf)
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting'
]
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
setup_configuration(get_args(args))
def test_start(mocker, fee, default_conf, caplog) -> None:
"""
Test start() function
@@ -604,9 +627,13 @@ def test_backtest_record(default_conf, fee, mocker):
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"open_index": [1, 119, 153, 185],
"close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14]})
"trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True]
})
backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4
# Assert file_dump_json was only called once
@@ -617,12 +644,16 @@ def test_backtest_record(default_conf, fee, mocker):
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records
oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur) in records:
for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end) in records:
assert pair == 'UNITTEST/BTC'
isinstance(profit, float)
assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints
isinstance(date_buy, str)
isinstance(date_sell, str)
assert isinstance(date_buy, float)
assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix:
assert buy_index > oix

View File

@@ -25,7 +25,7 @@ def prec_satoshi(a, b) -> float:
# Unit tests
def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
"""
Test rpc_trade_status() method
"""
@@ -36,7 +36,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -71,7 +72,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
assert trade.find('[ETH/BTC]') >= 0
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
"""
Test rpc_status_table() method
"""
@@ -82,7 +83,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -104,7 +106,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
def test_rpc_daily_profit(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test rpc_daily_profit() method
"""
@@ -115,7 +117,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -155,7 +158,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test rpc_trade_statistics() method
"""
@@ -170,7 +173,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -230,7 +234,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
# Test that rpc_trade_statistics can handle trades that lacks
# trade.open_rate (it is set to None)
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
ticker_sell_up, limit_buy_order, limit_sell_order):
"""
Test rpc_trade_statistics() method
@@ -246,7 +250,8 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -386,7 +391,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
assert freqtradebot.state == State.STOPPED
def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
"""
Test rpc_forcesell() method
"""
@@ -408,6 +413,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
}
),
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -489,7 +495,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
limit_sell_order, markets, mocker) -> None:
"""
Test rpc_performance() method
"""
@@ -501,7 +507,8 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -527,7 +534,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert prec_satoshi(res[0]['profit'], 6.2)
def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
"""
Test rpc_count() method
"""
@@ -540,6 +547,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)

View File

@@ -185,7 +185,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
)
def test_status(default_conf, update, mocker, fee, ticker) -> None:
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
"""
Test _status() method
"""
@@ -202,6 +202,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None:
get_ticker=ticker,
get_pair_detail_url=MagicMock(),
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
status_table = MagicMock()
@@ -230,7 +231,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None:
assert status_table.call_count == 1
def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _status() method
"""
@@ -241,6 +242,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
status_table = MagicMock()
@@ -276,7 +278,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0]
def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _status_table() method
"""
@@ -288,6 +290,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
get_ticker=ticker,
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
mocker.patch.multiple(
@@ -329,7 +332,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
limit_sell_order, markets, mocker) -> None:
"""
Test _daily() method
"""
@@ -343,7 +346,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
mocker.patch.multiple(
@@ -441,7 +445,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test _profit() method
"""
@@ -452,7 +456,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
mocker.patch.multiple(
@@ -705,7 +710,8 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None:
def test_forcesell_handle(default_conf, update, ticker, fee,
ticker_sell_up, markets, mocker) -> None:
"""
Test _forcesell() method
"""
@@ -718,7 +724,8 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, moc
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -745,7 +752,8 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, moc
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_down, mocker) -> None:
def test_forcesell_down_handle(default_conf, update, ticker, fee,
ticker_sell_down, markets, mocker) -> None:
"""
Test _forcesell() method
"""
@@ -758,7 +766,8 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_do
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -789,7 +798,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_do
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None:
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _forcesell() method
"""
@@ -803,7 +812,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
@@ -867,7 +877,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
def test_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test _performance() method
"""
@@ -883,7 +893,8 @@ def test_performance_handle(default_conf, update, ticker, fee,
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf)
@@ -931,7 +942,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
assert 'not running' in msg_mock.call_args_list[0][0][0]
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _count() method
"""
@@ -947,7 +958,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value={'id': 'mocked_order_id'})
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
get_markets=markets
)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
freqtradebot = FreqtradeBot(default_conf)

View File

@@ -50,13 +50,16 @@ def test_load_strategy(result):
assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_strategy_custom_directory(result):
def test_load_strategy_invalid_directory(result, caplog):
resolver = StrategyResolver()
extra_dir = os.path.join('some', 'path')
with pytest.raises(
FileNotFoundError,
match=r".*No such file or directory: '{}'".format(extra_dir)):
resolver._load_strategy('TestStrategy', extra_dir)
resolver._load_strategy('TestStrategy', extra_dir)
assert (
'freqtrade.strategy.resolver',
logging.WARNING,
'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples
assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result)

View File

@@ -42,6 +42,7 @@ def test_analyze_object() -> None:
assert hasattr(Analyze, 'get_signal')
assert hasattr(Analyze, 'should_sell')
assert hasattr(Analyze, 'min_roi_reached')
assert hasattr(Analyze, 'stop_loss_reached')
def test_dataframe_correct_length(result):

View File

@@ -55,6 +55,18 @@ def test_load_config_missing_attributes(default_conf) -> None:
configuration._validate_config(conf)
def test_load_config_incorrect_stake_amount(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
conf['stake_amount'] = 'fake'
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
configuration = Configuration(Namespace())
configuration._validate_config(conf)
def test_load_config_file(default_conf, mocker, caplog) -> None:
"""
Test Configuration._load_config_file() method

View File

@@ -14,7 +14,7 @@ import arrow
import pytest
import requests
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade import constants, DependencyException, OperationalException, TemporaryError
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade
from freqtrade.state import State
@@ -216,7 +216,238 @@ def test_refresh_whitelist() -> None:
pass
def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
"""
Test get_trade_stake_amount() method
"""
patch_RPCManager(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2)
)
freqtrade = FreqtradeBot(default_conf)
result = freqtrade._get_trade_stake_amount()
assert(result == default_conf['stake_amount'])
def test_get_trade_stake_amount_no_stake_amount(default_conf,
ticker,
limit_buy_order,
fee,
mocker) -> None:
"""
Test get_trade_stake_amount() method
"""
patch_RPCManager(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)
)
# test defined stake amount
freqtrade = FreqtradeBot(default_conf)
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
freqtrade._get_trade_stake_amount()
def test_get_trade_stake_amount_unlimited_amount(default_conf,
ticker,
limit_buy_order,
fee,
markets,
mocker) -> None:
"""
Test get_trade_stake_amount() method
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_balance=MagicMock(return_value=default_conf['stake_amount']),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
conf['max_open_trades'] = 2
freqtrade = FreqtradeBot(conf)
# no open trades, order amount should be 'balance / max_open_trades'
result = freqtrade._get_trade_stake_amount()
assert result == default_conf['stake_amount'] / conf['max_open_trades']
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
freqtrade.create_trade()
result = freqtrade._get_trade_stake_amount()
assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1)
# create 2 trades, order amount should be None
freqtrade.create_trade()
result = freqtrade._get_trade_stake_amount()
assert result is None
# set max_open_trades = None, so do not trade
conf['max_open_trades'] = 0
freqtrade = FreqtradeBot(conf)
result = freqtrade._get_trade_stake_amount()
assert result is None
def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
"""
Test get_trade_stake_amount() method
"""
patch_RPCManager(mocker)
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.freqtradebot.Analyze.get_stoploss', MagicMock(return_value=-0.05))
freqtrade = FreqtradeBot(default_conf)
# no pair found
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC'
}])
)
with pytest.raises(ValueError, match=r'.*get market information.*'):
freqtrade._get_min_pair_stake_amount('BNB/BTC', 1)
# no 'limits' section
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC'
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result is None
# empty 'limits' section
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result is None
# no cost Min
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {"min": None},
'amount': {}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result is None
# no amount Min
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {},
'amount': {"min": None}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result is None
# empty 'cost'/'amount' section
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {},
'amount': {}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result is None
# min cost is set
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {'min': 2},
'amount': {}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result == 2 / 0.9
# min amount is set
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {},
'amount': {'min': 2}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2)
assert result == 2 * 2 / 0.9
# min amount and cost are set (cost is minimal)
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {'min': 2},
'amount': {'min': 2}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2)
assert result == min(2, 2 * 2) / 0.9
# min amount and cost are set (amount is minial)
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {'min': 8},
'amount': {'min': 2}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2)
assert result == min(8, 2 * 2) / 0.9
def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None:
"""
Test create_trade() method
"""
@@ -229,6 +460,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
# Save state of current whitelist
@@ -252,32 +484,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non
assert whitelist == default_conf['exchange']['pair_whitelist']
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
"""
Test create_trade() method
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=buy_mock,
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['stake_amount'] = 0.0005
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
assert rate * amount >= conf['stake_amount']
def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
fee, markets, mocker) -> None:
"""
Test create_trade() method
"""
@@ -291,6 +499,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
get_fee=fee,
get_markets=markets
)
freqtrade = FreqtradeBot(default_conf)
@@ -298,7 +507,87 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee
freqtrade.create_trade()
def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order,
fee, markets, mocker) -> None:
"""
Test create_trade() method
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=buy_mock,
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['stake_amount'] = 0.0005
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
assert rate * amount >= conf['stake_amount']
def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order,
fee, markets, mocker) -> None:
"""
Test create_trade() method
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=buy_mock,
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['stake_amount'] = 0.000000005
freqtrade = FreqtradeBot(conf)
result = freqtrade.create_trade()
assert result is False
def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
fee, markets, mocker) -> None:
"""
Test create_trade() method
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_balance=MagicMock(return_value=default_conf['stake_amount']),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['max_open_trades'] = 0
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
freqtrade = FreqtradeBot(conf)
assert freqtrade.create_trade() is False
assert freqtrade._get_trade_stake_amount() is None
def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None:
"""
Test create_trade() method
"""
@@ -311,6 +600,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocke
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
@@ -325,7 +615,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocke
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
limit_buy_order, fee, mocker) -> None:
limit_buy_order, fee, markets, mocker) -> None:
"""
Test create_trade() method
"""
@@ -338,6 +628,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
@@ -616,7 +907,8 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf,
assert log_has('Unable to sell trade: ', caplog.record_tuples)
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
fee, markets, mocker) -> None:
"""
Test check_handle() method
"""
@@ -632,7 +924,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee
get_fee=fee,
get_markets=markets
)
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
@@ -660,7 +953,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock
assert trade.close_date is not None
def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order,
fee, markets, mocker) -> None:
"""
Test check_handle() method
"""
@@ -677,6 +971,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee,
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
freqtrade = FreqtradeBot(conf)
@@ -718,7 +1013,8 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee,
assert freqtrade.handle_trade(trades[0]) is True
def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None:
def test_handle_trade_roi(default_conf, ticker, limit_buy_order,
fee, mocker, markets, caplog) -> None:
"""
Test check_handle() method
"""
@@ -735,6 +1031,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, ca
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True)
@@ -755,7 +1052,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, ca
def test_handle_trade_experimental(
default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None:
default_conf, ticker, limit_buy_order, fee, mocker, markets, caplog) -> None:
"""
Test check_handle() method
"""
@@ -772,6 +1069,7 @@ def test_handle_trade_experimental(
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
@@ -789,7 +1087,8 @@ def test_handle_trade_experimental(
assert log_has('Sell signal received. Selling..', caplog.record_tuples)
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fee, mocker) -> None:
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
fee, markets, mocker) -> None:
"""
Test check_handle() method
"""
@@ -802,6 +1101,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fe
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
freqtrade = FreqtradeBot(default_conf)
@@ -852,7 +1152,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit
freqtrade.check_handle_timedout(600)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
@@ -893,7 +1193,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
Trade.session.add(trade_sell)
# check it does cancel sell orders over the time limit
freqtrade.check_handle_timedout(600)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert trade_sell.is_open is True
@@ -933,7 +1233,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
# check it does cancel buy orders over the time limit
# note this is for a partially-complete buy order
freqtrade.check_handle_timedout(600)
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
@@ -984,7 +1284,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
'recent call last):\n.*'
)
freqtrade.check_handle_timedout(600)
freqtrade.check_handle_timedout()
assert filter(regexp.match, caplog.record_tuples)
@@ -1040,7 +1340,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
assert cancel_order_mock.call_count == 1
def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None:
"""
Test execute_sell() method with a ticker going UP
"""
@@ -1051,7 +1351,8 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
freqtrade = FreqtradeBot(default_conf)
@@ -1081,7 +1382,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None:
def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None:
"""
Test execute_sell() method with a ticker going DOWN
"""
@@ -1093,7 +1394,8 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtrade = FreqtradeBot(default_conf)
@@ -1122,7 +1424,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
ticker_sell_up, mocker) -> None:
ticker_sell_up, markets, mocker) -> None:
"""
Test execute_sell() method with a ticker going DOWN and with a bot config empty
"""
@@ -1133,7 +1435,8 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtrade = FreqtradeBot(default_conf)
@@ -1163,7 +1466,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
ticker_sell_down, mocker) -> None:
ticker_sell_down, markets, mocker) -> None:
"""
Test execute_sell() method with a ticker going DOWN and with a bot config empty
"""
@@ -1174,7 +1477,8 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee
get_fee=fee,
get_markets=markets
)
freqtrade = FreqtradeBot(default_conf)
@@ -1201,7 +1505,8 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mocker) -> None:
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
fee, markets, mocker) -> None:
"""
Test sell_profit_only feature when enabled
"""
@@ -1219,6 +1524,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mock
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['experimental'] = {
@@ -1234,7 +1540,8 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mock
assert freqtrade.handle_trade(trade) is True
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, mocker) -> None:
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
fee, markets, mocker) -> None:
"""
Test sell_profit_only feature when disabled
"""
@@ -1252,6 +1559,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, moc
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['experimental'] = {
@@ -1267,14 +1575,14 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, moc
assert freqtrade.handle_trade(trade) is True
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker) -> None:
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
mocker.patch('freqtrade.freqtradebot.Analyze.stop_loss_reached', return_value=False)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
@@ -1285,6 +1593,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['experimental'] = {
@@ -1300,7 +1609,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker
assert freqtrade.handle_trade(trade) is False
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocker) -> None:
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
@@ -1312,12 +1621,13 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
'bid': 0.0000172,
'ask': 0.0000173,
'last': 0.0000172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
@@ -1335,6 +1645,183 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke
assert freqtrade.handle_trade(trade) is True
def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.0000172,
'ask': 0.0000173,
'last': 0.0000172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['experimental'] = {
'ignore_roi_if_buy_signal': True
}
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
trade.update(limit_buy_order)
patch_get_signal(mocker, value=(True, True))
assert freqtrade.handle_trade(trade) is False
# Test if buy-signal is absent (should sell due to roi = true)
patch_get_signal(mocker, value=(False, True))
assert freqtrade.handle_trade(trade) is True
def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000102,
'ask': 0.00000103,
'last': 0.00000102
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['trailing_stop'] = True
print(limit_buy_order)
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
trade.update(limit_buy_order)
caplog.set_level(logging.DEBUG)
# Sell as trailing-stop is reached
assert freqtrade.handle_trade(trade) is True
assert log_has(
f'HIT STOP: current price at 0.000001, stop loss is {trade.stop_loss:.6f}, '
f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples)
def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
buy_price = limit_buy_order['price']
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': buy_price - 0.000001,
'ask': buy_price - 0.000001,
'last': buy_price - 0.000001
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['trailing_stop'] = True
conf['trailing_stop_positive'] = 0.01
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
trade.update(limit_buy_order)
caplog.set_level(logging.DEBUG)
# stop-loss not reached
assert freqtrade.handle_trade(trade) is False
# Raise ticker above buy price
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
MagicMock(return_value={
'bid': buy_price + 0.000003,
'ask': buy_price + 0.000003,
'last': buy_price + 0.000003
}))
# stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False
assert log_has(f'using positive stop loss mode: 0.01 since we have profit 0.26662643',
caplog.record_tuples)
assert log_has(f'adjusted stop loss', caplog.record_tuples)
assert trade.stop_loss == 0.0000138501
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
MagicMock(return_value={
'bid': buy_price + 0.000002,
'ask': buy_price + 0.000002,
'last': buy_price + 0.000002
}))
# Lower price again (but still positive)
assert freqtrade.handle_trade(trade) is True
assert log_has(
f'HIT STOP: current price at {buy_price + 0.000002:.6f}, '
f'stop loss is {trade.stop_loss:.6f}, '
f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples)
def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
fee, markets, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['experimental'] = {
'ignore_roi_if_buy_signal': False
}
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
trade.update(limit_buy_order)
# Sell due to min_roi_reached
patch_get_signal(mocker, value=(True, True))
assert freqtrade.handle_trade(trade) is True
# Test if buy-signal is absent
patch_get_signal(mocker, value=(False, True))
assert freqtrade.handle_trade(trade) is True
def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
"""
Test get_real_amount - fee in quote currency

View File

@@ -7,6 +7,7 @@ from sqlalchemy import create_engine
from freqtrade import constants, OperationalException
from freqtrade.persistence import Trade, init, clean_dry_run_db
from freqtrade.tests.conftest import log_has
@pytest.fixture(scope='function')
@@ -14,9 +15,7 @@ def init_persistence(default_conf):
init(default_conf)
def test_init_create_session(default_conf, mocker):
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
def test_init_create_session(default_conf):
# Check if init create a session
init(default_conf)
assert hasattr(Trade, 'session')
@@ -29,20 +28,17 @@ def test_init_custom_db_url(default_conf, mocker):
# Update path to a value other than default, but still in-memory
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
mocker.patch.dict('freqtrade.persistence._CONF', conf)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
def test_init_invalid_db_url(default_conf, mocker):
def test_init_invalid_db_url(default_conf):
conf = deepcopy(default_conf)
# Update path to a value other than default, but still in-memory
conf.update({'db_url': 'unknown:///some.url'})
mocker.patch.dict('freqtrade.persistence._CONF', conf)
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init(conf)
@@ -53,7 +49,6 @@ def test_init_prod_db(default_conf, mocker):
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
mocker.patch.dict('freqtrade.persistence._CONF', conf)
init(conf)
assert create_engine_mock.call_count == 1
@@ -66,7 +61,6 @@ def test_init_dryrun_db(default_conf, mocker):
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
mocker.patch.dict('freqtrade.persistence._CONF', conf)
init(conf)
assert create_engine_mock.call_count == 1
@@ -407,9 +401,12 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "bittrex"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
def test_migrate_new(mocker, default_conf, fee):
def test_migrate_new(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
@@ -446,6 +443,11 @@ def test_migrate_new(mocker, default_conf, fee):
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
# fake previous backup
engine.execute("create table trades_bak as select * from trades")
engine.execute("create table trades_bak1 as select * from trades")
# Run init to test migration
init(default_conf)
@@ -460,3 +462,54 @@ def test_migrate_new(mocker, default_conf, fee):
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert log_has("trying trades_bak1", caplog.record_tuples)
assert log_has("trying trades_bak2", caplog.record_tuples)
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
assert trade.stop_loss == 0.95
assert trade.max_rate == 1
assert trade.initial_stop_loss == 0.95
# Get percent of profit with a lowre rate
trade.adjust_stop_loss(0.96, 0.05)
assert trade.stop_loss == 0.95
assert trade.max_rate == 1
assert trade.initial_stop_loss == 0.95
# Get percent of profit with a custom rate (Higher than open rate)
trade.adjust_stop_loss(1.3, -0.1)
assert round(trade.stop_loss, 8) == 1.17
assert trade.max_rate == 1.3
assert trade.initial_stop_loss == 0.95
# current rate lower again ... should not change
trade.adjust_stop_loss(1.2, 0.1)
assert round(trade.stop_loss, 8) == 1.17
assert trade.max_rate == 1.3
assert trade.initial_stop_loss == 0.95
# current rate higher... should raise stoploss
trade.adjust_stop_loss(1.4, 0.1)
assert round(trade.stop_loss, 8) == 1.26
assert trade.max_rate == 1.4
assert trade.initial_stop_loss == 0.95
# Initial is true but stop_loss set - so doesn't do anything
trade.adjust_stop_loss(1.7, 0.1, True)
assert round(trade.stop_loss, 8) == 1.26
assert trade.max_rate == 1.4
assert trade.initial_stop_loss == 0.95