Partial exit using average price (#6545)

Introduce Partial exits
This commit is contained in:
Kavinkumar
2022-07-31 17:49:04 +05:30
committed by GitHub
parent 369c6da5d8
commit a4bada3ebe
20 changed files with 1462 additions and 347 deletions

View File

@@ -843,8 +843,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
# In case of closed order
order['status'] = 'closed'
order['price'] = 10
order['cost'] = 100
order['average'] = 10
order['cost'] = 300
order['id'] = '444'
mocker.patch('freqtrade.exchange.Exchange.create_order',
@@ -855,7 +855,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
assert trade
assert trade.open_order_id is None
assert trade.open_rate == 10
assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8)
assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8)
assert pytest.approx(trade.liquidation_price) == liq_price
# In case of rejected or expired order and partially filled
@@ -863,8 +863,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
order['amount'] = 30.0
order['filled'] = 20.0
order['remaining'] = 10.00
order['price'] = 0.5
order['cost'] = 15.0
order['average'] = 0.5
order['cost'] = 10.0
order['id'] = '555'
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=order))
@@ -872,9 +872,9 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
trade = Trade.query.all()[3]
trade.is_short = is_short
assert trade
assert trade.open_order_id == '555'
assert trade.open_order_id is None
assert trade.open_rate == 0.5
assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8)
assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8)
# Test with custom stake
order['status'] = 'open'
@@ -901,7 +901,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
order['amount'] = 30.0 * leverage
order['filled'] = 0.0
order['remaining'] = 30.0
order['price'] = 0.5
order['average'] = 0.5
order['cost'] = 0.0
order['id'] = '66'
mocker.patch('freqtrade.exchange.Exchange.create_order',
@@ -1083,7 +1083,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
'last': 1.9
}),
create_order=MagicMock(side_effect=[
{'id': enter_order['id']},
enter_order,
exit_order,
]),
get_fee=fee,
@@ -1109,20 +1109,20 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
# should do nothing and return false
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
trade.stoploss_order_id = "100"
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id == 100
assert trade.stoploss_order_id == "100"
# Third case: when stoploss was set but it was canceled for some reason
# should set a stoploss immediately and return False
caplog.clear()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
trade.stoploss_order_id = "100"
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order)
@@ -2039,6 +2039,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit
trade = MagicMock()
trade.open_order_id = '123'
trade.amount = 123
# Test raise of OperationalException exception
mocker.patch(
@@ -2352,9 +2353,9 @@ def test_close_trade(
trade.is_short = is_short
assert trade
oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], 'buy')
oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], trade.enter_side)
trade.update_trade(oobj)
oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], 'sell')
oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], trade.exit_side)
trade.update_trade(oobj)
assert trade.is_open is False
@@ -2397,8 +2398,8 @@ def test_manage_open_orders_entry_usercustom(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt,
fetch_order=MagicMock(return_value=old_order),
cancel_order_with_result=cancel_order_wr_mock,
cancel_order=cancel_order_mock,
cancel_order_with_result=cancel_order_wr_mock,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf_usdt)
@@ -2446,7 +2447,9 @@ def test_manage_open_orders_entry(
) -> None:
old_order = limit_sell_order_old if is_short else limit_buy_order_old
rpc_mock = patch_RPCManager(mocker)
old_order['id'] = open_trade.open_order_id
open_trade.open_order_id = old_order['id']
order = Order.parse_from_ccxt_object(old_order, 'mocked', 'buy')
open_trade.orders[0] = order
limit_buy_cancel = deepcopy(old_order)
limit_buy_cancel['status'] = 'canceled'
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
@@ -2637,7 +2640,9 @@ def test_manage_open_orders_exit_usercustom(
is_short, open_trade_usdt, caplog
) -> None:
default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1}
limit_sell_order_old['id'] = open_trade_usdt.open_order_id
open_trade_usdt.open_order_id = limit_sell_order_old['id']
order = Order.parse_from_ccxt_object(limit_sell_order_old, 'mocked', 'sell')
open_trade_usdt.orders[0] = order
if is_short:
limit_sell_order_old['side'] = 'buy'
open_trade_usdt.is_short = is_short
@@ -3250,6 +3255,9 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg
@@ -3310,6 +3318,9 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg
@@ -3391,6 +3402,9 @@ def test_execute_trade_exit_custom_exit_price(
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg
@@ -3459,6 +3473,9 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg
@@ -3690,7 +3707,7 @@ def test_execute_trade_exit_market_order(
)
assert not trade.is_open
assert trade.close_profit == profit_ratio
assert pytest.approx(trade.close_profit) == profit_ratio
assert rpc_mock.call_count == 4
last_msg = rpc_mock.call_args_list[-2][0][0]
@@ -3718,6 +3735,9 @@ def test_execute_trade_exit_market_order(
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
'sub_trade': False,
'cumulative_profit': 0.0,
'stake_amount': pytest.approx(60),
} == last_msg
@@ -3789,7 +3809,7 @@ def test_exit_profit_only(
'last': bid
}),
create_order=MagicMock(side_effect=[
limit_order_open[eside],
limit_order[eside],
{'id': 1234553382},
]),
get_fee=fee,
@@ -4081,7 +4101,7 @@ def test_trailing_stop_loss_positive(
'last': enter_price - (-0.01 if is_short else 0.01),
}),
create_order=MagicMock(side_effect=[
limit_order_open[eside],
limit_order[eside],
{'id': 1234553382},
]),
get_fee=fee,
@@ -4632,7 +4652,7 @@ def test_order_book_entry_pricing1(mocker, default_conf_usdt, order_book_l2, exc
with pytest.raises(PricingError):
freqtrade.exchange.get_rate('ETH/USDT', side="entry", is_short=False, refresh=True)
assert log_has_re(
r'Entry Price at location 1 from orderbook could not be determined.', caplog)
r'ETH/USDT - Entry Price at location 1 from orderbook could not be determined.', caplog)
else:
assert freqtrade.exchange.get_rate(
'ETH/USDT', side="entry", is_short=False, refresh=True) == 0.043935
@@ -4711,8 +4731,9 @@ def test_order_book_exit_pricing(
return_value={'bids': [[]], 'asks': [[]]})
with pytest.raises(PricingError):
freqtrade.handle_trade(trade)
assert log_has_re(r'Exit Price at location 1 from orderbook could not be determined\..*',
caplog)
assert log_has_re(
r"ETH/USDT - Exit Price at location 1 from orderbook could not be determined\..*",
caplog)
def test_startup_state(default_conf_usdt, mocker):
@@ -5385,7 +5406,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
'status': None,
'price': 9,
'amount': 12,
'cost': 100,
'cost': 108,
'ft_is_open': True,
'id': '651',
'order_id': '651'
@@ -5480,7 +5501,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
assert trade.open_order_id is None
assert pytest.approx(trade.open_rate) == 9.90909090909
assert trade.amount == 22
assert trade.stake_amount == 218
assert pytest.approx(trade.stake_amount) == 218
orders = Order.query.all()
assert orders
@@ -5533,6 +5554,329 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
# Make sure the closed order is found as the second order.
order = trade.select_order('buy', False)
assert order.order_id == '652'
closed_sell_dca_order_1 = {
'ft_pair': pair,
'status': 'closed',
'ft_order_side': 'sell',
'side': 'sell',
'type': 'limit',
'price': 8,
'average': 8,
'amount': 15,
'filled': 15,
'cost': 120,
'ft_is_open': False,
'id': '653',
'order_id': '653'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_sell_dca_order_1))
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
MagicMock(return_value=closed_sell_dca_order_1))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_sell_dca_order_1))
assert freqtrade.execute_trade_exit(trade=trade, limit=8,
exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
sub_trade_amt=15)
# Assert trade is as expected (averaged dca)
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None
assert trade.is_open
assert trade.amount == 22
assert trade.stake_amount == 192.05405405405406
assert pytest.approx(trade.open_rate) == 8.729729729729
orders = Order.query.all()
assert orders
assert len(orders) == 4
# Make sure the closed order is found as the second order.
order = trade.select_order('sell', False)
assert order.order_id == '653'
def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
"""
TODO: Should be adjusted to test both long and short
buy 100 @ 11
sell 50 @ 8
sell 50 @ 16
"""
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_wallet(mocker, free=10000)
default_conf_usdt.update({
"position_adjustment_enable": True,
"dry_run": False,
"stake_amount": 200.0,
"dry_run_wallet": 1000.0,
})
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
bid = 11
amount = 100
buy_rate_mock = MagicMock(return_value=bid)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_rate=buy_rate_mock,
fetch_ticker=MagicMock(return_value={
'bid': 10,
'ask': 12,
'last': 11
}),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
pair = 'ETH/USDT'
# Initial buy
closed_successful_buy_order = {
'pair': pair,
'ft_pair': pair,
'ft_order_side': 'buy',
'side': 'buy',
'type': 'limit',
'status': 'closed',
'price': bid,
'average': bid,
'cost': bid * amount,
'amount': amount,
'filled': amount,
'ft_is_open': False,
'id': '600',
'order_id': '600'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_successful_buy_order))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_successful_buy_order))
assert freqtrade.execute_entry(pair, amount)
# Should create an closed trade with an no open order id
# Order is filled and trade is open
orders = Order.query.all()
assert orders
assert len(orders) == 1
trade = Trade.query.first()
assert trade
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.open_rate == bid
assert trade.stake_amount == bid * amount
# Assume it does nothing since order is closed and trade is open
freqtrade.update_closed_trades_without_assigned_fees()
trade = Trade.query.first()
assert trade
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.open_rate == bid
assert trade.stake_amount == bid * amount
assert not trade.fee_updated(trade.entry_side)
freqtrade.manage_open_orders()
trade = Trade.query.first()
assert trade
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.open_rate == bid
assert trade.stake_amount == bid * amount
assert not trade.fee_updated(trade.entry_side)
amount = 50
ask = 8
closed_sell_dca_order_1 = {
'ft_pair': pair,
'status': 'closed',
'ft_order_side': 'sell',
'side': 'sell',
'type': 'limit',
'price': ask,
'average': ask,
'amount': amount,
'filled': amount,
'cost': amount * ask,
'ft_is_open': False,
'id': '601',
'order_id': '601'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_sell_dca_order_1))
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
MagicMock(return_value=closed_sell_dca_order_1))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_sell_dca_order_1))
assert freqtrade.execute_trade_exit(trade=trade, limit=ask,
exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
sub_trade_amt=amount)
trades: List[Trade] = trade.get_open_trades_without_assigned_fees()
assert len(trades) == 1
# Assert trade is as expected (averaged dca)
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None
assert trade.amount == 50
assert trade.open_rate == 11
assert trade.stake_amount == 550
assert pytest.approx(trade.realized_profit) == -152.375
assert pytest.approx(trade.close_profit_abs) == -152.375
orders = Order.query.all()
assert orders
assert len(orders) == 2
# Make sure the closed order is found as the second order.
order = trade.select_order('sell', False)
assert order.order_id == '601'
amount = 50
ask = 16
closed_sell_dca_order_2 = {
'ft_pair': pair,
'status': 'closed',
'ft_order_side': 'sell',
'side': 'sell',
'type': 'limit',
'price': ask,
'average': ask,
'amount': amount,
'filled': amount,
'cost': amount * ask,
'ft_is_open': False,
'id': '602',
'order_id': '602'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_sell_dca_order_2))
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
MagicMock(return_value=closed_sell_dca_order_2))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_sell_dca_order_2))
assert freqtrade.execute_trade_exit(trade=trade, limit=ask,
exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
sub_trade_amt=amount)
# Assert trade is as expected (averaged dca)
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None
assert trade.amount == 50
assert trade.open_rate == 11
assert trade.stake_amount == 550
# Trade fully realized
assert pytest.approx(trade.realized_profit) == 94.25
assert pytest.approx(trade.close_profit_abs) == 94.25
orders = Order.query.all()
assert orders
assert len(orders) == 3
# Make sure the closed order is found as the second order.
order = trade.select_order('sell', False)
assert order.order_id == '602'
assert trade.is_open is False
@pytest.mark.parametrize('data', [
(
# tuple 1 - side amount, price
# tuple 2 - amount, open_rate, stake_amount, cumulative_profit, realized_profit, rel_profit
(('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)),
(('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)),
(('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)),
(('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)),
(('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, 336.625, 0.1343142)), # final profit (sum)
),
(
(('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)),
(('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)),
(('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)),
(('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)),
(('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)),
(('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 3175.75, 0.9747170)), # final profit
)
])
def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None:
default_conf_usdt.update({
"position_adjustment_enable": True,
"dry_run": False,
"stake_amount": 200.0,
"dry_run_wallet": 1000.0,
})
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_wallet(mocker, free=10000)
freqtrade = FreqtradeBot(default_conf_usdt)
trade = None
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
for idx, (order, result) in enumerate(data):
amount = order[1]
price = order[2]
price_mock = MagicMock(return_value=price)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_rate=price_mock,
fetch_ticker=MagicMock(return_value={
'bid': 10,
'ask': 12,
'last': 11
}),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
pair = 'ETH/USDT'
closed_successful_order = {
'pair': pair,
'ft_pair': pair,
'ft_order_side': order[0],
'side': order[0],
'type': 'limit',
'status': 'closed',
'price': price,
'average': price,
'cost': price * amount,
'amount': amount,
'filled': amount,
'ft_is_open': False,
'id': f'60{idx}',
'order_id': f'60{idx}'
}
mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=closed_successful_order))
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
MagicMock(return_value=closed_successful_order))
if order[0] == 'buy':
assert freqtrade.execute_entry(pair, amount, trade=trade)
else:
assert freqtrade.execute_trade_exit(
trade=trade, limit=price,
exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
sub_trade_amt=amount)
orders1 = Order.query.all()
assert orders1
assert len(orders1) == idx + 1
trade = Trade.query.first()
assert trade
if idx < len(data) - 1:
assert trade.is_open is True
assert trade.open_order_id is None
assert trade.amount == result[0]
assert trade.open_rate == result[1]
assert trade.stake_amount == result[2]
assert pytest.approx(trade.realized_profit) == result[3]
assert pytest.approx(trade.close_profit_abs) == result[4]
assert pytest.approx(trade.close_profit) == result[5]
order_obj = trade.select_order(order[0], False)
assert order_obj.order_id == f'60{idx}'
trade = Trade.query.first()
assert trade
assert trade.open_order_id is None
assert trade.is_open is False
def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None:
@@ -5556,9 +5900,25 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca
"max_entry_position_adjustment": 0,
})
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
buy_rate_mock = MagicMock(return_value=10)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_rate=buy_rate_mock,
fetch_ticker=MagicMock(return_value={
'bid': 10,
'ask': 12,
'last': 11
}),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
create_mock_trades(fee)
caplog.set_level(logging.DEBUG)
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=10)
freqtrade.process_open_trade_positions()
assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)
caplog.clear()
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-10)
freqtrade.process_open_trade_positions()
assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog)