Merge pull request #1341 from mishaker/stoploss_on_exchange

Stoploss on exchange
This commit is contained in:
Matthias
2018-12-01 09:46:37 +01:00
committed by GitHub
14 changed files with 495 additions and 31 deletions

View File

@@ -26,20 +26,21 @@ def log_has(line, logs):
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.validate_timeframes', 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="bittrex"))
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
if api_mock:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
else:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
def get_patched_exchange(mocker, config, api_mock=None) -> Exchange:
patch_exchange(mocker, api_mock)
def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange:
patch_exchange(mocker, api_mock, id)
exchange = Exchange(config)
return exchange

View File

@@ -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._load_markets', MagicMock(return_value={}))
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)
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
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,
match=r'Exchange .* does not support market orders.'):
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):
api_mock = MagicMock()
@@ -1122,3 +1145,85 @@ def test_get_fee(default_conf, mocker):
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'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

View File

@@ -189,7 +189,8 @@ def test_strategy_override_order_types(caplog):
order_types = {
'buy': 'market',
'sell': 'limit',
'stoploss': 'limit'
'stoploss': 'limit',
'stoploss_on_exchange': True,
}
config = {
@@ -199,13 +200,14 @@ def test_strategy_override_order_types(caplog):
resolver = StrategyResolver(config)
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 ('freqtrade.resolvers.strategy_resolver',
logging.INFO,
"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
config = {
@@ -263,13 +265,13 @@ def test_call_deprecated_function(result, monkeypatch):
assert resolver.strategy._sell_fun_len == 2
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
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
assert type(buydf) is DataFrame
assert isinstance(buydf, DataFrame)
assert 'buy' in buydf.columns
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
assert type(selldf) is DataFrame
assert isinstance(selldf, DataFrame)
assert 'sell' in selldf

View File

@@ -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
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:
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
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,
ticker_sell_up, markets, mocker) -> None:
rpc_mock = patch_RPCManager(mocker)

View File

@@ -426,6 +426,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
max_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
ticker_interval INTEGER,
PRIMARY KEY (id),
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.strategy 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_bak2", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak2",