conflict resolved0
This commit is contained in:
commit
237dc8290f
@ -36,7 +36,8 @@
|
|||||||
"order_types": {
|
"order_types": {
|
||||||
"buy": "limit",
|
"buy": "limit",
|
||||||
"sell": "limit",
|
"sell": "limit",
|
||||||
"stoploss": "market"
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": "false"
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
|
@ -39,7 +39,7 @@ The table below will list all configuration parameters.
|
|||||||
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
|
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
|
||||||
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`).
|
| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`).
|
||||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||||
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
||||||
@ -141,17 +141,18 @@ end up paying more then would probably have been necessary.
|
|||||||
|
|
||||||
### Understand order_types
|
### Understand order_types
|
||||||
|
|
||||||
`order_types` contains a dict mapping order-types to market-types. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market.
|
`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled.
|
||||||
This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.
|
This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.
|
||||||
|
|
||||||
If this is configured, all 3 values (`"buy"`, `"sell"` and `"stoploss"`) need to be present, otherwise the bot warn about it and will fail to start.
|
If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start.
|
||||||
The below is the default which is used if this is not configured in either Strategy or configuration.
|
The below is the default which is used if this is not configured in either Strategy or configuration.
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
"order_types": {
|
"order_types": {
|
||||||
"buy": "limit",
|
"buy": "limit",
|
||||||
"sell": "limit",
|
"sell": "limit",
|
||||||
"stoploss": "market"
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ DEFAULT_HYPEROPT = 'DefaultHyperOpts'
|
|||||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||||
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||||
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss']
|
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
|
|
||||||
|
|
||||||
@ -109,9 +109,10 @@ CONF_SCHEMA = {
|
|||||||
'properties': {
|
'properties': {
|
||||||
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}
|
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
|
'stoploss_on_exchange': {'type': 'boolean'}
|
||||||
},
|
},
|
||||||
'required': ['buy', 'sell', 'stoploss']
|
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
},
|
},
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
'edge': {'$ref': '#/definitions/edge'},
|
'edge': {'$ref': '#/definitions/edge'},
|
||||||
|
@ -228,6 +228,12 @@ class Exchange(object):
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self.name} does not support market orders.')
|
f'Exchange {self.name} does not support market orders.')
|
||||||
|
|
||||||
|
if order_types.get('stoploss_on_exchange'):
|
||||||
|
if self.name is not 'Binance':
|
||||||
|
raise OperationalException(
|
||||||
|
'On exchange stoploss is not supported for %s.' % self.name
|
||||||
|
)
|
||||||
|
|
||||||
def exchange_has(self, endpoint: str) -> bool:
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if exchange implements a specific API endpoint.
|
Checks if exchange implements a specific API endpoint.
|
||||||
@ -334,6 +340,61 @@ class Exchange(object):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||||
|
"""
|
||||||
|
creates a stoploss limit order.
|
||||||
|
NOTICE: it is not supported by all exchanges. only binance is tested for now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
|
amount = self.symbol_amount_prec(pair, amount)
|
||||||
|
rate = self.symbol_price_prec(pair, rate)
|
||||||
|
stop_price = self.symbol_price_prec(pair, stop_price)
|
||||||
|
|
||||||
|
# Ensure rate is less than stop price
|
||||||
|
if stop_price <= rate:
|
||||||
|
raise OperationalException(
|
||||||
|
'In stoploss limit order, stop price should be more than limit price')
|
||||||
|
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
|
self._dry_run_open_orders[order_id] = {
|
||||||
|
'info': {},
|
||||||
|
'id': order_id,
|
||||||
|
'pair': pair,
|
||||||
|
'price': stop_price,
|
||||||
|
'amount': amount,
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'side': 'sell',
|
||||||
|
'remaining': amount,
|
||||||
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
|
'status': 'open',
|
||||||
|
'fee': None
|
||||||
|
}
|
||||||
|
return self._dry_run_open_orders[order_id]
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._api.create_order(pair, 'stop_loss_limit', 'sell',
|
||||||
|
amount, rate, {'stopPrice': stop_price})
|
||||||
|
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Insufficient funds to place stoploss limit order on market {pair}. '
|
||||||
|
f'Tried to put a stoploss amount {amount} with '
|
||||||
|
f'stop {stop_price} and limit {rate} (total {rate*amount}).'
|
||||||
|
f'Message: {e}')
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Could not place stoploss limit order on market {pair}.'
|
||||||
|
f'Tried to place stoploss amount {amount} with '
|
||||||
|
f'stop {stop_price} and limit {rate} (total {rate*amount}).'
|
||||||
|
f'Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not place stoploss limit order due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_balance(self, currency: str) -> float:
|
def get_balance(self, currency: str) -> float:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
|
@ -54,6 +54,7 @@ class FreqtradeBot(object):
|
|||||||
# Init objects
|
# Init objects
|
||||||
self.config = config
|
self.config = config
|
||||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
self.rpc: RPCManager = RPCManager(self)
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
self.persistence = None
|
self.persistence = None
|
||||||
self.exchange = Exchange(self.config)
|
self.exchange = Exchange(self.config)
|
||||||
@ -107,7 +108,7 @@ class FreqtradeBot(object):
|
|||||||
})
|
})
|
||||||
logger.info('Changing state to: %s', state.name)
|
logger.info('Changing state to: %s', state.name)
|
||||||
if state == State.RUNNING:
|
if state == State.RUNNING:
|
||||||
self._startup_messages()
|
self.rpc.startup_messages(self.config)
|
||||||
|
|
||||||
if state == State.STOPPED:
|
if state == State.STOPPED:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@ -121,38 +122,6 @@ class FreqtradeBot(object):
|
|||||||
min_secs=min_secs)
|
min_secs=min_secs)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def _startup_messages(self) -> None:
|
|
||||||
if self.config.get('dry_run', False):
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
|
||||||
'status': 'Dry run is enabled. All trades are simulated.'
|
|
||||||
})
|
|
||||||
stake_currency = self.config['stake_currency']
|
|
||||||
stake_amount = self.config['stake_amount']
|
|
||||||
minimal_roi = self.config['minimal_roi']
|
|
||||||
ticker_interval = self.config['ticker_interval']
|
|
||||||
exchange_name = self.config['exchange']['name']
|
|
||||||
strategy_name = self.config.get('strategy', '')
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
|
||||||
'status': f'*Exchange:* `{exchange_name}`\n'
|
|
||||||
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
|
||||||
f'*Minimum ROI:* `{minimal_roi}`\n'
|
|
||||||
f'*Ticker Interval:* `{ticker_interval}`\n'
|
|
||||||
f'*Strategy:* `{strategy_name}`'
|
|
||||||
})
|
|
||||||
if self.config.get('dynamic_whitelist', False):
|
|
||||||
top_pairs = 'top volume ' + str(self.config.get('dynamic_whitelist', 20))
|
|
||||||
specific_pairs = ''
|
|
||||||
else:
|
|
||||||
top_pairs = 'whitelisted'
|
|
||||||
specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', ''))
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
|
||||||
'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...'
|
|
||||||
f'{specific_pairs}'
|
|
||||||
})
|
|
||||||
|
|
||||||
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
"""
|
"""
|
||||||
Throttles the given callable that it
|
Throttles the given callable that it
|
||||||
@ -491,6 +460,7 @@ class FreqtradeBot(object):
|
|||||||
'stake_currency': stake_currency,
|
'stake_currency': stake_currency,
|
||||||
'fiat_currency': fiat_currency
|
'fiat_currency': fiat_currency
|
||||||
})
|
})
|
||||||
|
|
||||||
# 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
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
@ -507,6 +477,7 @@ class FreqtradeBot(object):
|
|||||||
strategy=self.strategy.get_strategy_name(),
|
strategy=self.strategy.get_strategy_name(),
|
||||||
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
|
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
|
||||||
)
|
)
|
||||||
|
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
@ -555,6 +526,12 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
trade.update(order)
|
trade.update(order)
|
||||||
|
|
||||||
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open:
|
||||||
|
result = self.handle_stoploss_on_exchange(trade)
|
||||||
|
if result:
|
||||||
|
self.wallets.update()
|
||||||
|
return result
|
||||||
|
|
||||||
if trade.is_open and trade.open_order_id is None:
|
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
|
||||||
result = self.handle_trade(trade)
|
result = self.handle_trade(trade)
|
||||||
@ -656,9 +633,50 @@ class FreqtradeBot(object):
|
|||||||
if self.check_sell(trade, sell_rate, buy, sell):
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
logger.debug('Found no sell signal for %s.', trade)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
|
||||||
|
"""
|
||||||
|
Check if trade is fulfilled in which case the stoploss
|
||||||
|
on exchange should be added immediately if stoploss on exchnage
|
||||||
|
is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = False
|
||||||
|
|
||||||
|
# If trade is open and the buy order is fulfilled but there is no stoploss,
|
||||||
|
# then we add a stoploss on exchange
|
||||||
|
if not trade.open_order_id and not trade.stoploss_order_id:
|
||||||
|
if self.edge:
|
||||||
|
stoploss = self.edge.stoploss(pair=trade.pair)
|
||||||
|
else:
|
||||||
|
stoploss = self.strategy.stoploss
|
||||||
|
|
||||||
|
stop_price = trade.open_rate * (1 + stoploss)
|
||||||
|
|
||||||
|
# limit price should be less than stop price.
|
||||||
|
# 0.98 is arbitrary here.
|
||||||
|
limit_price = stop_price * 0.98
|
||||||
|
|
||||||
|
stoploss_order_id = self.exchange.stoploss_limit(
|
||||||
|
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
|
||||||
|
)['id']
|
||||||
|
trade.stoploss_order_id = str(stoploss_order_id)
|
||||||
|
|
||||||
|
# Or the trade open and there is already a stoploss on exchange.
|
||||||
|
# so we check if it is hit ...
|
||||||
|
elif trade.stoploss_order_id:
|
||||||
|
logger.debug('Handling stoploss on exchange %s ...', trade)
|
||||||
|
order = self.exchange.get_order(trade.stoploss_order_id, trade.pair)
|
||||||
|
if order['status'] == 'closed':
|
||||||
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
|
trade.update(order)
|
||||||
|
result = True
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||||
if self.edge:
|
if self.edge:
|
||||||
stoploss = self.edge.stoploss(trade.pair)
|
stoploss = self.edge.stoploss(trade.pair)
|
||||||
@ -784,6 +802,10 @@ class FreqtradeBot(object):
|
|||||||
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||||
sell_type = 'stoploss'
|
sell_type = 'stoploss'
|
||||||
|
|
||||||
|
# First cancelling stoploss on exchange ...
|
||||||
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||||
|
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
|
||||||
|
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order_id = self.exchange.sell(pair=str(trade.pair),
|
order_id = self.exchange.sell(pair=str(trade.pair),
|
||||||
ordertype=self.strategy.order_types[sell_type],
|
ordertype=self.strategy.order_types[sell_type],
|
||||||
|
@ -66,6 +66,7 @@ class Backtesting(object):
|
|||||||
if self.config.get('strategy_list', None):
|
if self.config.get('strategy_list', None):
|
||||||
# Force one interval
|
# Force one interval
|
||||||
self.ticker_interval = str(self.config.get('ticker_interval'))
|
self.ticker_interval = str(self.config.get('ticker_interval'))
|
||||||
|
self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]
|
||||||
for strat in list(self.config['strategy_list']):
|
for strat in list(self.config['strategy_list']):
|
||||||
stratconf = deepcopy(self.config)
|
stratconf = deepcopy(self.config)
|
||||||
stratconf['strategy'] = strat
|
stratconf['strategy'] = strat
|
||||||
@ -86,6 +87,8 @@ class Backtesting(object):
|
|||||||
"""
|
"""
|
||||||
self.strategy = strategy
|
self.strategy = strategy
|
||||||
self.ticker_interval = self.config.get('ticker_interval')
|
self.ticker_interval = self.config.get('ticker_interval')
|
||||||
|
self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]
|
||||||
|
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
|
||||||
self.advise_buy = strategy.advise_buy
|
self.advise_buy = strategy.advise_buy
|
||||||
self.advise_sell = strategy.advise_sell
|
self.advise_sell = strategy.advise_sell
|
||||||
|
|
||||||
@ -280,8 +283,13 @@ class Backtesting(object):
|
|||||||
processed = args['processed']
|
processed = args['processed']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
position_stacking = args.get('position_stacking', False)
|
position_stacking = args.get('position_stacking', False)
|
||||||
|
start_date = args['start_date']
|
||||||
|
end_date = args['end_date']
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock: Dict = {}
|
trade_count_lock: Dict = {}
|
||||||
|
ticker: Dict = {}
|
||||||
|
pairs = []
|
||||||
|
# Create ticker dict
|
||||||
for pair, pair_data in processed.items():
|
for pair, pair_data in processed.items():
|
||||||
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||||
|
|
||||||
@ -296,15 +304,28 @@ class Backtesting(object):
|
|||||||
|
|
||||||
# Convert from Pandas to list for performance reasons
|
# Convert from Pandas to list for performance reasons
|
||||||
# (Looping Pandas is slow.)
|
# (Looping Pandas is slow.)
|
||||||
ticker = [x for x in ticker_data.itertuples()]
|
ticker[pair] = [x for x in ticker_data.itertuples()]
|
||||||
|
pairs.append(pair)
|
||||||
|
|
||||||
|
lock_pair_until: Dict = {}
|
||||||
|
tmp = start_date + timedelta(minutes=self.ticker_interval_mins)
|
||||||
|
index = 0
|
||||||
|
# Loop timerange and test per pair
|
||||||
|
while tmp < end_date:
|
||||||
|
# print(f"time: {tmp}")
|
||||||
|
for i, pair in enumerate(ticker):
|
||||||
|
try:
|
||||||
|
row = ticker[pair][index]
|
||||||
|
except IndexError:
|
||||||
|
# missing Data for one pair ...
|
||||||
|
# Warnings for this are shown by `validate_backtest_data`
|
||||||
|
continue
|
||||||
|
|
||||||
lock_pair_until = None
|
|
||||||
for index, row in enumerate(ticker):
|
|
||||||
if row.buy == 0 or row.sell == 1:
|
if row.buy == 0 or row.sell == 1:
|
||||||
continue # skip rows where no buy signal or that would immediately sell off
|
continue # skip rows where no buy signal or that would immediately sell off
|
||||||
|
|
||||||
if not position_stacking:
|
if not position_stacking:
|
||||||
if lock_pair_until is not None and row.date <= lock_pair_until:
|
if pair in lock_pair_until and row.date <= lock_pair_until[pair]:
|
||||||
continue
|
continue
|
||||||
if max_open_trades > 0:
|
if max_open_trades > 0:
|
||||||
# Check if max_open_trades has already been reached for the given date
|
# Check if max_open_trades has already been reached for the given date
|
||||||
@ -313,17 +334,19 @@ class Backtesting(object):
|
|||||||
|
|
||||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||||
|
|
||||||
trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][index + 1:],
|
||||||
trade_count_lock, args)
|
trade_count_lock, args)
|
||||||
|
|
||||||
if trade_entry:
|
if trade_entry:
|
||||||
lock_pair_until = trade_entry.close_time
|
lock_pair_until[pair] = trade_entry.close_time
|
||||||
trades.append(trade_entry)
|
trades.append(trade_entry)
|
||||||
else:
|
else:
|
||||||
# Set lock_pair_until to end of testing period if trade could not be closed
|
# Set lock_pair_until to end of testing period if trade could not be closed
|
||||||
# This happens only if the buy-signal was with the last candle
|
# This happens only if the buy-signal was with the last candle
|
||||||
lock_pair_until = ticker_data.iloc[-1].date
|
lock_pair_until[pair] = end_date
|
||||||
|
|
||||||
|
tmp += timedelta(minutes=self.ticker_interval_mins)
|
||||||
|
index += 1
|
||||||
return DataFrame.from_records(trades, columns=BacktestResult._fields)
|
return DataFrame.from_records(trades, columns=BacktestResult._fields)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
@ -390,6 +413,8 @@ class Backtesting(object):
|
|||||||
'processed': preprocessed,
|
'processed': preprocessed,
|
||||||
'max_open_trades': max_open_trades,
|
'max_open_trades': max_open_trades,
|
||||||
'position_stacking': self.config.get('position_stacking', False),
|
'position_stacking': self.config.get('position_stacking', False),
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ from skopt.space import Dimension
|
|||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.optimize import load_data
|
from freqtrade.optimize import load_data, get_timeframe
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.resolvers import HyperOptResolver
|
from freqtrade.resolvers import HyperOptResolver
|
||||||
|
|
||||||
@ -167,11 +167,14 @@ class Hyperopt(Backtesting):
|
|||||||
self.strategy.stoploss = params['stoploss']
|
self.strategy.stoploss = params['stoploss']
|
||||||
|
|
||||||
processed = load(TICKERDATA_PICKLE)
|
processed = load(TICKERDATA_PICKLE)
|
||||||
|
min_date, max_date = get_timeframe(processed)
|
||||||
results = self.backtest(
|
results = self.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': self.config['stake_amount'],
|
'stake_amount': self.config['stake_amount'],
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
'position_stacking': self.config.get('position_stacking', True),
|
'position_stacking': self.config.get('position_stacking', True),
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result_explanation = self.format_results(results)
|
result_explanation = self.format_results(results)
|
||||||
|
@ -82,7 +82,7 @@ def check_migrate(engine) -> None:
|
|||||||
logger.debug(f'trying {table_back_name}')
|
logger.debug(f'trying {table_back_name}')
|
||||||
|
|
||||||
# Check for latest column
|
# Check for latest column
|
||||||
if not has_column(cols, 'ticker_interval'):
|
if not has_column(cols, 'stoploss_order_id'):
|
||||||
logger.info(f'Running database migration - backup available as {table_back_name}')
|
logger.info(f'Running database migration - backup available as {table_back_name}')
|
||||||
|
|
||||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||||
@ -91,6 +91,7 @@ def check_migrate(engine) -> None:
|
|||||||
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
|
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
|
||||||
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
||||||
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
||||||
|
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
|
||||||
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||||
strategy = get_column_def(cols, 'strategy', 'null')
|
strategy = get_column_def(cols, 'strategy', 'null')
|
||||||
@ -106,7 +107,7 @@ def check_migrate(engine) -> None:
|
|||||||
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
||||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
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, sell_reason, strategy,
|
stop_loss, initial_stop_loss, stoploss_order_id, max_rate, sell_reason, strategy,
|
||||||
ticker_interval
|
ticker_interval
|
||||||
)
|
)
|
||||||
select id, lower(exchange),
|
select id, lower(exchange),
|
||||||
@ -122,7 +123,8 @@ def check_migrate(engine) -> None:
|
|||||||
{close_rate_requested} close_rate_requested, close_profit,
|
{close_rate_requested} 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} stop_loss, {initial_stop_loss} initial_stop_loss,
|
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
||||||
{max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy,
|
{stoploss_order_id} stoploss_order_id, {max_rate} max_rate,
|
||||||
|
{sell_reason} sell_reason, {strategy} strategy,
|
||||||
{ticker_interval} ticker_interval
|
{ticker_interval} ticker_interval
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
""")
|
""")
|
||||||
@ -177,6 +179,8 @@ class Trade(_DECL_BASE):
|
|||||||
stop_loss = Column(Float, nullable=True, default=0.0)
|
stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
# absolute value of the initial stop loss
|
# absolute value of the initial stop loss
|
||||||
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# stoploss order id which is on exchange
|
||||||
|
stoploss_order_id = Column(String, nullable=True, index=True)
|
||||||
# absolute value of the highest reached price
|
# absolute value of the highest reached price
|
||||||
max_rate = Column(Float, nullable=True, default=0.0)
|
max_rate = Column(Float, nullable=True, default=0.0)
|
||||||
sell_reason = Column(String, nullable=True)
|
sell_reason = Column(String, nullable=True)
|
||||||
@ -249,6 +253,10 @@ class Trade(_DECL_BASE):
|
|||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
elif order_type == 'limit' and order['side'] == 'sell':
|
elif order_type == 'limit' and order['side'] == 'sell':
|
||||||
self.close(order['price'])
|
self.close(order['price'])
|
||||||
|
elif order_type == 'stop_loss_limit':
|
||||||
|
self.stoploss_order_id = None
|
||||||
|
logger.info('STOP_LOSS_LIMIT is hit for %s.', self)
|
||||||
|
self.close(order['average'])
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unknown order type: {order_type}')
|
raise ValueError(f'Unknown order type: {order_type}')
|
||||||
cleanup()
|
cleanup()
|
||||||
|
@ -4,7 +4,7 @@ This module contains class to manage RPC communications (Telegram, Slack, ...)
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC, RPCMessageType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -51,3 +51,35 @@ class RPCManager(object):
|
|||||||
for mod in self.registered_modules:
|
for mod in self.registered_modules:
|
||||||
logger.debug('Forwarding message to rpc.%s', mod.name)
|
logger.debug('Forwarding message to rpc.%s', mod.name)
|
||||||
mod.send_msg(msg)
|
mod.send_msg(msg)
|
||||||
|
|
||||||
|
def startup_messages(self, config) -> None:
|
||||||
|
if config.get('dry_run', False):
|
||||||
|
self.send_msg({
|
||||||
|
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||||
|
'status': 'Dry run is enabled. All trades are simulated.'
|
||||||
|
})
|
||||||
|
stake_currency = config['stake_currency']
|
||||||
|
stake_amount = config['stake_amount']
|
||||||
|
minimal_roi = config['minimal_roi']
|
||||||
|
ticker_interval = config['ticker_interval']
|
||||||
|
exchange_name = config['exchange']['name']
|
||||||
|
strategy_name = config.get('strategy', '')
|
||||||
|
self.send_msg({
|
||||||
|
'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
||||||
|
'status': f'*Exchange:* `{exchange_name}`\n'
|
||||||
|
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
||||||
|
f'*Minimum ROI:* `{minimal_roi}`\n'
|
||||||
|
f'*Ticker Interval:* `{ticker_interval}`\n'
|
||||||
|
f'*Strategy:* `{strategy_name}`'
|
||||||
|
})
|
||||||
|
if config.get('dynamic_whitelist', False):
|
||||||
|
top_pairs = 'top volume ' + str(config.get('dynamic_whitelist', 20))
|
||||||
|
specific_pairs = ''
|
||||||
|
else:
|
||||||
|
top_pairs = 'whitelisted'
|
||||||
|
specific_pairs = '\n' + ', '.join(config['exchange'].get('pair_whitelist', ''))
|
||||||
|
self.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...'
|
||||||
|
f'{specific_pairs}'
|
||||||
|
})
|
||||||
|
@ -32,7 +32,8 @@ class DefaultStrategy(IStrategy):
|
|||||||
order_types = {
|
order_types = {
|
||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
'stoploss': 'limit'
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
@ -33,6 +33,7 @@ class SellType(Enum):
|
|||||||
"""
|
"""
|
||||||
ROI = "roi"
|
ROI = "roi"
|
||||||
STOP_LOSS = "stop_loss"
|
STOP_LOSS = "stop_loss"
|
||||||
|
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
||||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||||
SELL_SIGNAL = "sell_signal"
|
SELL_SIGNAL = "sell_signal"
|
||||||
FORCE_SELL = "force_sell"
|
FORCE_SELL = "force_sell"
|
||||||
@ -74,7 +75,8 @@ class IStrategy(ABC):
|
|||||||
order_types: Dict = {
|
order_types: Dict = {
|
||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
'stoploss': 'limit'
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
|
|
||||||
# run "populate_indicators" only for new candle
|
# run "populate_indicators" only for new candle
|
||||||
@ -221,11 +223,17 @@ class IStrategy(ABC):
|
|||||||
# Set current rate to low for backtesting sell
|
# Set current rate to low for backtesting sell
|
||||||
current_rate = low or rate
|
current_rate = low or rate
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
|
|
||||||
|
if self.order_types.get('stoploss_on_exchange'):
|
||||||
|
stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||||
|
else:
|
||||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||||
current_time=date, current_profit=current_profit,
|
current_time=date, current_profit=current_profit,
|
||||||
force_stoploss=force_stoploss)
|
force_stoploss=force_stoploss)
|
||||||
|
|
||||||
if stoplossflag.sell_flag:
|
if stoplossflag.sell_flag:
|
||||||
return stoplossflag
|
return stoplossflag
|
||||||
|
|
||||||
# Set current rate to low for backtesting sell
|
# Set current rate to low for backtesting sell
|
||||||
current_rate = high or rate
|
current_rate = high or rate
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
|
@ -27,20 +27,21 @@ def log_has(line, logs):
|
|||||||
False)
|
False)
|
||||||
|
|
||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None) -> None:
|
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value="Bittrex"))
|
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value="bittrex"))
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
||||||
|
|
||||||
if api_mock:
|
if api_mock:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
else:
|
else:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
||||||
|
|
||||||
|
|
||||||
def get_patched_exchange(mocker, config, api_mock=None) -> Exchange:
|
def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange:
|
||||||
patch_exchange(mocker, api_mock)
|
patch_exchange(mocker, api_mock, id)
|
||||||
exchange = Exchange(config)
|
exchange = Exchange(config)
|
||||||
return exchange
|
return exchange
|
||||||
|
|
||||||
|
@ -362,18 +362,41 @@ def test_validate_order_types(default_conf, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market'}
|
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
||||||
|
default_conf['order_types'] = {
|
||||||
|
'buy': 'limit',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'market',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
|
}
|
||||||
|
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
|
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
|
||||||
default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market'}
|
default_conf['order_types'] = {
|
||||||
|
'buy': 'limit',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'market',
|
||||||
|
'stoploss_on_exchange': 'false'
|
||||||
|
}
|
||||||
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'Exchange .* does not support market orders.'):
|
match=r'Exchange .* does not support market orders.'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
default_conf['order_types'] = {
|
||||||
|
'buy': 'limit',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': True
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'On exchange stoploss is not supported for .*'):
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_order_types_not_in_config(default_conf, mocker):
|
def test_validate_order_types_not_in_config(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -1122,3 +1145,85 @@ def test_get_fee(default_conf, mocker):
|
|||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
'get_fee', 'calculate_fee')
|
'get_fee', 'calculate_fee')
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_limit_order(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
order_type = 'stop_loss_limit'
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
|
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][4] == 200
|
||||||
|
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'stop_loss_limit'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
@ -4,9 +4,10 @@ import arrow
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from freqtrade.constants import TICKER_INTERVAL_MINUTES
|
||||||
|
|
||||||
ticker_start_time = arrow.get(2018, 10, 3)
|
ticker_start_time = arrow.get(2018, 10, 3)
|
||||||
ticker_interval_in_minute = 60
|
tests_ticker_interval = "1h"
|
||||||
|
|
||||||
|
|
||||||
class BTrade(NamedTuple):
|
class BTrade(NamedTuple):
|
||||||
@ -30,8 +31,8 @@ class BTContainer(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
def _get_frame_time_from_offset(offset):
|
def _get_frame_time_from_offset(offset):
|
||||||
return ticker_start_time.shift(
|
return ticker_start_time.shift(minutes=(offset * TICKER_INTERVAL_MINUTES[tests_ticker_interval])
|
||||||
minutes=(offset * ticker_interval_in_minute)).datetime.replace(tzinfo=None)
|
).datetime.replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
def _build_backtest_dataframe(ticker_with_signals):
|
def _build_backtest_dataframe(ticker_with_signals):
|
||||||
|
@ -6,10 +6,11 @@ from pandas import DataFrame
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
from freqtrade.optimize import get_timeframe
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataframe,
|
from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataframe,
|
||||||
_get_frame_time_from_offset)
|
_get_frame_time_from_offset, tests_ticker_interval)
|
||||||
from freqtrade.tests.conftest import patch_exchange
|
from freqtrade.tests.conftest import patch_exchange
|
||||||
|
|
||||||
|
|
||||||
@ -147,6 +148,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
"""
|
"""
|
||||||
default_conf["stoploss"] = data.stop_loss
|
default_conf["stoploss"] = data.stop_loss
|
||||||
default_conf["minimal_roi"] = {"0": data.roi}
|
default_conf["minimal_roi"] = {"0": data.roi}
|
||||||
|
default_conf['ticker_interval'] = tests_ticker_interval
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0))
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0))
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
frame = _build_backtest_dataframe(data.data)
|
frame = _build_backtest_dataframe(data.data)
|
||||||
@ -158,29 +160,21 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
pair = 'UNITTEST/BTC'
|
pair = 'UNITTEST/BTC'
|
||||||
# Dummy data as we mock the analyze functions
|
# Dummy data as we mock the analyze functions
|
||||||
data_processed = {pair: DataFrame()}
|
data_processed = {pair: DataFrame()}
|
||||||
|
min_date, max_date = get_timeframe({pair: frame})
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': default_conf['stake_amount'],
|
'stake_amount': default_conf['stake_amount'],
|
||||||
'processed': data_processed,
|
'processed': data_processed,
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
print(results.T)
|
print(results.T)
|
||||||
|
|
||||||
assert len(results) == len(data.trades)
|
assert len(results) == len(data.trades)
|
||||||
assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3)
|
assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3)
|
||||||
# if data.sell_r == SellType.STOP_LOSS:
|
|
||||||
# assert log_has("Stop loss hit.", caplog.record_tuples)
|
|
||||||
# else:
|
|
||||||
# assert not log_has("Stop loss hit.", caplog.record_tuples)
|
|
||||||
# log_test = (f'Force_selling still open trade UNITTEST/BTC with '
|
|
||||||
# f'{results.iloc[-1].profit_percent} perc - {results.iloc[-1].profit_abs}')
|
|
||||||
# if data.sell_r == SellType.FORCE_SELL:
|
|
||||||
# assert log_has(log_test,
|
|
||||||
# caplog.record_tuples)
|
|
||||||
# else:
|
|
||||||
# assert not log_has(log_test,
|
|
||||||
# caplog.record_tuples)
|
|
||||||
for c, trade in enumerate(data.trades):
|
for c, trade in enumerate(data.trades):
|
||||||
res = results.iloc[c]
|
res = results.iloc[c]
|
||||||
assert res.sell_reason == trade.sell_reason
|
assert res.sell_reason == trade.sell_reason
|
||||||
|
@ -13,6 +13,7 @@ from arrow import Arrow
|
|||||||
|
|
||||||
from freqtrade import DependencyException, constants, optimize
|
from freqtrade import DependencyException, constants, optimize
|
||||||
from freqtrade.arguments import Arguments, TimeRange
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
|
from freqtrade.optimize import get_timeframe
|
||||||
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
||||||
start)
|
start)
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
@ -86,17 +87,21 @@ def load_data_test(what):
|
|||||||
|
|
||||||
def simple_backtest(config, contour, num_results, mocker) -> None:
|
def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
config['ticker_interval'] = '1m'
|
||||||
backtesting = Backtesting(config)
|
backtesting = Backtesting(config)
|
||||||
|
|
||||||
data = load_data_test(contour)
|
data = load_data_test(contour)
|
||||||
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(processed)
|
||||||
assert isinstance(processed, dict)
|
assert isinstance(processed, dict)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': config['stake_amount'],
|
'stake_amount': config['stake_amount'],
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
'max_open_trades': 1,
|
'max_open_trades': 1,
|
||||||
'position_stacking': False
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||||
@ -123,12 +128,16 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
|||||||
data = trim_dictlist(data, -201)
|
data = trim_dictlist(data, -201)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(conf)
|
backtesting = Backtesting(conf)
|
||||||
|
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(processed)
|
||||||
return {
|
return {
|
||||||
'stake_amount': conf['stake_amount'],
|
'stake_amount': conf['stake_amount'],
|
||||||
'processed': backtesting.strategy.tickerdata_to_dataframe(data),
|
'processed': processed,
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
'position_stacking': False,
|
'position_stacking': False,
|
||||||
'record': record
|
'record': record,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -449,7 +458,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
default_conf['ticker_interval'] = "1m"
|
default_conf['ticker_interval'] = '1m'
|
||||||
default_conf['live'] = False
|
default_conf['live'] = False
|
||||||
default_conf['datadir'] = None
|
default_conf['datadir'] = None
|
||||||
default_conf['export'] = None
|
default_conf['export'] = None
|
||||||
@ -505,12 +514,15 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
|||||||
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
|
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
|
||||||
data = trim_dictlist(data, -200)
|
data = trim_dictlist(data, -200)
|
||||||
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(data_processed)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': default_conf['stake_amount'],
|
'stake_amount': default_conf['stake_amount'],
|
||||||
'processed': data_processed,
|
'processed': data_processed,
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
'position_stacking': False
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
@ -554,12 +566,16 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
|||||||
# Run a backtesting for an exiting 5min ticker_interval
|
# Run a backtesting for an exiting 5min ticker_interval
|
||||||
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||||
data = trim_dictlist(data, -200)
|
data = trim_dictlist(data, -200)
|
||||||
|
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(processed)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': default_conf['stake_amount'],
|
'stake_amount': default_conf['stake_amount'],
|
||||||
'processed': backtesting.strategy.tickerdata_to_dataframe(data),
|
'processed': processed,
|
||||||
'max_open_trades': 1,
|
'max_open_trades': 1,
|
||||||
'position_stacking': False
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
@ -583,25 +599,13 @@ def test_processed(default_conf, mocker) -> None:
|
|||||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
tests = [['raise', 18], ['lower', 0], ['sine', 19]]
|
tests = [['raise', 18], ['lower', 0], ['sine', 19]]
|
||||||
|
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||||
|
default_conf['experimental'] = {"use_sell_signal": True}
|
||||||
|
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
simple_backtest(default_conf, contour, numres, mocker)
|
simple_backtest(default_conf, contour, numres, mocker)
|
||||||
|
|
||||||
|
|
||||||
# Test backtest using offline data (testdata directory)
|
|
||||||
def test_backtest_ticks(default_conf, fee, mocker):
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
ticks = [1, 5]
|
|
||||||
fun = Backtesting(default_conf).advise_buy
|
|
||||||
for _ in ticks:
|
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
|
||||||
backtesting = Backtesting(default_conf)
|
|
||||||
backtesting.advise_buy = fun # Override
|
|
||||||
backtesting.advise_sell = fun # Override
|
|
||||||
results = backtesting.backtest(backtest_conf)
|
|
||||||
assert not results.empty
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_clash_buy_sell(mocker, default_conf):
|
def test_backtest_clash_buy_sell(mocker, default_conf):
|
||||||
# Override the default buy trend function in our default_strategy
|
# Override the default buy trend function in our default_strategy
|
||||||
def fun(dataframe=None, pair=None):
|
def fun(dataframe=None, pair=None):
|
||||||
@ -636,14 +640,92 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.file_dump_json', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.file_dump_json', MagicMock())
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
||||||
|
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||||
|
default_conf['experimental'] = {"use_sell_signal": True}
|
||||||
|
default_conf['ticker_interval'] = '1m'
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.advise_buy = _trend_alternate # Override
|
backtesting.advise_buy = _trend_alternate # Override
|
||||||
backtesting.advise_sell = _trend_alternate # Override
|
backtesting.advise_sell = _trend_alternate # Override
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
backtesting._store_backtest_result("test_.json", results)
|
backtesting._store_backtest_result("test_.json", results)
|
||||||
assert len(results) == 4
|
# 200 candles in backtest data
|
||||||
|
# won't buy on first (shifted by 1)
|
||||||
|
# 100 buys signals
|
||||||
|
assert len(results) == 99
|
||||||
# One trade was force-closed at the end
|
# One trade was force-closed at the end
|
||||||
assert len(results.loc[results.open_at_end]) == 1
|
assert len(results.loc[results.open_at_end]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_multi_pair(default_conf, fee, mocker):
|
||||||
|
|
||||||
|
def evaluate_result_multi(results, freq, max_open_trades):
|
||||||
|
# Find overlapping trades by expanding each trade once per period
|
||||||
|
# and then counting overlaps
|
||||||
|
dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq))
|
||||||
|
for row in results[['open_time', 'close_time']].iterrows()]
|
||||||
|
deltas = [len(x) for x in dates]
|
||||||
|
dates = pd.Series(pd.concat(dates).values, name='date')
|
||||||
|
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
|
||||||
|
|
||||||
|
df2 = df2.astype(dtype={"open_time": "datetime64", "close_time": "datetime64"})
|
||||||
|
df2 = pd.concat([dates, df2], axis=1)
|
||||||
|
df2 = df2.set_index('date')
|
||||||
|
df_final = df2.resample(freq)[['pair']].count()
|
||||||
|
return df_final[df_final['pair'] > max_open_trades]
|
||||||
|
|
||||||
|
def _trend_alternate_hold(dataframe=None, metadata=None):
|
||||||
|
"""
|
||||||
|
Buy every 8th candle - sell every other 8th -2 (hold on to pairs a bit)
|
||||||
|
"""
|
||||||
|
multi = 8
|
||||||
|
dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0)
|
||||||
|
dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
|
||||||
|
if metadata['pair'] in('ETH/BTC', 'LTC/BTC'):
|
||||||
|
dataframe['buy'] = dataframe['buy'].shift(-4)
|
||||||
|
dataframe['sell'] = dataframe['sell'].shift(-4)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC']
|
||||||
|
data = optimize.load_data(None, ticker_interval='5m', pairs=pairs)
|
||||||
|
data = trim_dictlist(data, -500)
|
||||||
|
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||||
|
default_conf['experimental'] = {"use_sell_signal": True}
|
||||||
|
default_conf['ticker_interval'] = '5m'
|
||||||
|
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting.advise_buy = _trend_alternate_hold # Override
|
||||||
|
backtesting.advise_sell = _trend_alternate_hold # Override
|
||||||
|
|
||||||
|
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(data_processed)
|
||||||
|
backtest_conf = {
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': data_processed,
|
||||||
|
'max_open_trades': 3,
|
||||||
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
|
}
|
||||||
|
|
||||||
|
results = backtesting.backtest(backtest_conf)
|
||||||
|
|
||||||
|
# Make sure we have parallel trades
|
||||||
|
assert len(evaluate_result_multi(results, '5min', 2)) > 0
|
||||||
|
# make sure we don't have trades with more than configured max_open_trades
|
||||||
|
assert len(evaluate_result_multi(results, '5min', 3)) == 0
|
||||||
|
|
||||||
|
backtest_conf = {
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': data_processed,
|
||||||
|
'max_open_trades': 1,
|
||||||
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
|
}
|
||||||
|
results = backtesting.backtest(backtest_conf)
|
||||||
|
assert len(evaluate_result_multi(results, '5min', 1)) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_record(default_conf, fee, mocker):
|
def test_backtest_record(default_conf, fee, mocker):
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.optimize import load_tickerdata_file
|
||||||
from freqtrade.optimize.hyperopt import Hyperopt, start
|
from freqtrade.optimize.hyperopt import Hyperopt, start
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
@ -293,6 +294,10 @@ def test_generate_optimizer(mocker, default_conf) -> None:
|
|||||||
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
||||||
MagicMock(return_value=backtest_result)
|
MagicMock(return_value=backtest_result)
|
||||||
)
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||||
|
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||||
|
)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
||||||
|
|
||||||
|
@ -113,3 +113,23 @@ def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
||||||
assert len(rpc_manager.registered_modules) == 1
|
assert len(rpc_manager.registered_modules) == 1
|
||||||
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
|
|
||||||
|
|
||||||
|
def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
rpc_manager.startup_messages(default_conf)
|
||||||
|
|
||||||
|
assert telegram_mock.call_count == 3
|
||||||
|
assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status']
|
||||||
|
|
||||||
|
telegram_mock.reset_mock()
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
default_conf['dynamic_whitelist'] = 20
|
||||||
|
|
||||||
|
rpc_manager.startup_messages(default_conf)
|
||||||
|
assert telegram_mock.call_count == 3
|
||||||
|
assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status']
|
||||||
|
@ -189,7 +189,8 @@ def test_strategy_override_order_types(caplog):
|
|||||||
order_types = {
|
order_types = {
|
||||||
'buy': 'market',
|
'buy': 'market',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
'stoploss': 'limit'
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
@ -199,13 +200,14 @@ def test_strategy_override_order_types(caplog):
|
|||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert resolver.strategy.order_types
|
assert resolver.strategy.order_types
|
||||||
for method in ['buy', 'sell', 'stoploss']:
|
for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']:
|
||||||
assert resolver.strategy.order_types[method] == order_types[method]
|
assert resolver.strategy.order_types[method] == order_types[method]
|
||||||
|
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
"Override strategy 'order_types' with value in config file:"
|
"Override strategy 'order_types' with value in config file:"
|
||||||
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'}."
|
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit',"
|
||||||
|
" 'stoploss_on_exchange': True}."
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
@ -263,13 +265,13 @@ def test_call_deprecated_function(result, monkeypatch):
|
|||||||
assert resolver.strategy._sell_fun_len == 2
|
assert resolver.strategy._sell_fun_len == 2
|
||||||
|
|
||||||
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
|
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||||
assert type(indicator_df) is DataFrame
|
assert isinstance(indicator_df, DataFrame)
|
||||||
assert 'adx' in indicator_df.columns
|
assert 'adx' in indicator_df.columns
|
||||||
|
|
||||||
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
|
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
|
||||||
assert type(buydf) is DataFrame
|
assert isinstance(buydf, DataFrame)
|
||||||
assert 'buy' in buydf.columns
|
assert 'buy' in buydf.columns
|
||||||
|
|
||||||
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
|
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
|
||||||
assert type(selldf) is DataFrame
|
assert isinstance(selldf, DataFrame)
|
||||||
assert 'sell' in selldf
|
assert 'sell' in selldf
|
||||||
|
@ -874,6 +874,100 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non
|
|||||||
assert call_args['amount'] == stake_amount / fix_price
|
assert call_args['amount'] == stake_amount / fix_price
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||||
|
return_value=limit_buy_order['amount'])
|
||||||
|
|
||||||
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
|
||||||
|
trade = MagicMock()
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
trade.is_open = True
|
||||||
|
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert trade.stoploss_order_id == '13434334'
|
||||||
|
assert stoploss_limit.call_count == 1
|
||||||
|
assert trade.is_open is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||||
|
markets, limit_buy_order, limit_sell_order) -> None:
|
||||||
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets,
|
||||||
|
stoploss_limit=stoploss_limit
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# First case: when stoploss is not yet set but the order is open
|
||||||
|
# should get the stoploss order id immediately
|
||||||
|
# and should return false as no trade actually happened
|
||||||
|
trade = MagicMock()
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert stoploss_limit.call_count == 1
|
||||||
|
assert trade.stoploss_order_id == "13434334"
|
||||||
|
|
||||||
|
# Second case: when stoploss is set but it is not yet hit
|
||||||
|
# should do nothing and return false
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = 100
|
||||||
|
|
||||||
|
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', hanging_stoploss_order)
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert trade.stoploss_order_id == 100
|
||||||
|
|
||||||
|
# Third case: when stoploss is set and it is hit
|
||||||
|
# should unset stoploss_order_id and return true
|
||||||
|
# as a trade actually happened
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = 100
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
stoploss_order_hit = MagicMock(return_value={
|
||||||
|
'status': 'closed',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit)
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is True
|
||||||
|
assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog.record_tuples)
|
||||||
|
assert trade.stoploss_order_id is None
|
||||||
|
assert trade.is_open is False
|
||||||
|
|
||||||
|
|
||||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
@ -1468,6 +1562,129 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
|
|||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
||||||
|
ticker, fee, ticker_sell_up,
|
||||||
|
markets, mocker) -> None:
|
||||||
|
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
|
||||||
|
stoploss_limit = MagicMock(return_value={
|
||||||
|
'id': 123,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
cancel_order = MagicMock(return_value=True)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order)
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
|
||||||
|
# Increase the price and sell it
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
||||||
|
sell_reason=SellType.SELL_SIGNAL)
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
assert cancel_order.call_count == 1
|
||||||
|
assert rpc_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
|
||||||
|
ticker, fee,
|
||||||
|
limit_buy_order,
|
||||||
|
markets, mocker) -> None:
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
|
||||||
|
stoploss_limit = MagicMock(return_value={
|
||||||
|
'id': 123,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert trade
|
||||||
|
assert trade.stoploss_order_id == '123'
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
|
||||||
|
# Assuming stoploss on exchnage is hit
|
||||||
|
# stoploss_order_id should become None
|
||||||
|
# and trade should be sold at the price of stoploss
|
||||||
|
stoploss_limit_executed = MagicMock(return_value={
|
||||||
|
"id": "123",
|
||||||
|
"timestamp": 1542707426845,
|
||||||
|
"datetime": "2018-11-20T09:50:26.845Z",
|
||||||
|
"lastTradeTimestamp": None,
|
||||||
|
"symbol": "BTC/USDT",
|
||||||
|
"type": "stop_loss_limit",
|
||||||
|
"side": "sell",
|
||||||
|
"price": 1.08801,
|
||||||
|
"amount": 90.99181074,
|
||||||
|
"cost": 99.0000000032274,
|
||||||
|
"average": 1.08801,
|
||||||
|
"filled": 90.99181074,
|
||||||
|
"remaining": 0.0,
|
||||||
|
"status": "closed",
|
||||||
|
"fee": None,
|
||||||
|
"trades": None
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed)
|
||||||
|
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert trade.stoploss_order_id is None
|
||||||
|
assert trade.is_open is False
|
||||||
|
print(trade.sell_reason)
|
||||||
|
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
|
assert rpc_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
||||||
ticker_sell_up, markets, mocker) -> None:
|
ticker_sell_up, markets, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
@ -426,6 +426,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||||||
max_rate FLOAT,
|
max_rate FLOAT,
|
||||||
sell_reason VARCHAR,
|
sell_reason VARCHAR,
|
||||||
strategy VARCHAR,
|
strategy VARCHAR,
|
||||||
|
ticker_interval INTEGER,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CHECK (is_open IN (0, 1))
|
CHECK (is_open IN (0, 1))
|
||||||
);"""
|
);"""
|
||||||
@ -471,6 +472,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||||||
assert trade.sell_reason is None
|
assert trade.sell_reason is None
|
||||||
assert trade.strategy is None
|
assert trade.strategy is None
|
||||||
assert trade.ticker_interval is None
|
assert trade.ticker_interval is None
|
||||||
|
assert trade.stoploss_order_id is None
|
||||||
assert log_has("trying trades_bak1", caplog.record_tuples)
|
assert log_has("trying trades_bak1", caplog.record_tuples)
|
||||||
assert log_has("trying trades_bak2", caplog.record_tuples)
|
assert log_has("trying trades_bak2", caplog.record_tuples)
|
||||||
assert log_has("Running database migration - backup available as trades_bak2",
|
assert log_has("Running database migration - backup available as trades_bak2",
|
||||||
|
@ -52,7 +52,8 @@ class TestStrategy(IStrategy):
|
|||||||
order_types = {
|
order_types = {
|
||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
'stoploss': 'market'
|
'stoploss': 'market',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
Loading…
Reference in New Issue
Block a user