From 4abe2373e298de17bef05ddd7920d981f7de174b Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 29 Nov 2024 00:34:39 +0100 Subject: [PATCH] [Futures] fix margin computation and tests --- .../personal_data/positions/position.py | 4 ++ .../positions/types/inverse_position.py | 13 +++--- .../positions/types/linear_position.py | 3 +- .../basic_keywords/test_amount.py | 42 +++++++++++++++++++ .../personal_data/positions/test_position.py | 4 ++ .../positions/types/test_inverse_position.py | 10 ++--- 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/octobot_trading/personal_data/positions/position.py b/octobot_trading/personal_data/positions/position.py index 35d0308c6e..037a599778 100644 --- a/octobot_trading/personal_data/positions/position.py +++ b/octobot_trading/personal_data/positions/position.py @@ -209,6 +209,10 @@ def _update(self, position_id, exchange_position_id, symbol, currency, market, t # update side after quantity as it relies on self.quantity self._update_side(not entry_price) self._update_prices_if_necessary(mark_price) + if changed: + # ensure fee to close and margin are up to date now that all other attributes are set + self.update_fee_to_close() + self._update_margin() return changed async def ensure_position_initialized(self, **kwargs): diff --git a/octobot_trading/personal_data/positions/types/inverse_position.py b/octobot_trading/personal_data/positions/types/inverse_position.py index 4affc621cc..04dbdb6ebf 100644 --- a/octobot_trading/personal_data/positions/types/inverse_position.py +++ b/octobot_trading/personal_data/positions/types/inverse_position.py @@ -102,14 +102,17 @@ def get_bankruptcy_price(self, price, side, with_mark_price=False): Short position = (Entry Price x Leverage) / (Leverage - 1) """ try: + price = self.mark_price if with_mark_price else price if side is enums.PositionSide.LONG: - return (self.mark_price if with_mark_price else - price * self.symbol_contract.current_leverage) \ + return ( + price * self.symbol_contract.current_leverage / (self.symbol_contract.current_leverage + constants.ONE) + ) elif side is enums.PositionSide.SHORT: - return (self.mark_price if with_mark_price else - price * self.symbol_contract.current_leverage) \ + return ( + price * self.symbol_contract.current_leverage / (self.symbol_contract.current_leverage - constants.ONE) + ) return constants.ZERO except (decimal.DivisionByZero, decimal.InvalidOperation): return constants.ZERO @@ -134,7 +137,7 @@ def get_fee_to_close(self, quantity, price, side, symbol, with_mark_price=False) :return: Fee to open = (Quantity * Mark Price) x Taker fee """ try: - return quantity / \ + return abs(quantity) / \ self.get_bankruptcy_price(price, side, with_mark_price=with_mark_price) * self.get_taker_fee(symbol) except (decimal.DivisionByZero, decimal.InvalidOperation): return constants.ZERO diff --git a/octobot_trading/personal_data/positions/types/linear_position.py b/octobot_trading/personal_data/positions/types/linear_position.py index a770f98fda..5fa60b2d35 100644 --- a/octobot_trading/personal_data/positions/types/linear_position.py +++ b/octobot_trading/personal_data/positions/types/linear_position.py @@ -109,7 +109,7 @@ def get_fee_to_close(self, quantity, price, side, symbol, with_mark_price=False) """ :return: Fee to open = (Quantity * Mark Price) x Taker fee """ - return quantity * self.get_bankruptcy_price(price, side, with_mark_price=with_mark_price) * \ + return abs(quantity) * self.get_bankruptcy_price(price, side, with_mark_price=with_mark_price) * \ self.get_taker_fee(symbol) def get_order_cost(self): @@ -124,6 +124,7 @@ def update_fee_to_close(self): """ self.fee_to_close = self.get_fee_to_close(self.size, self.entry_price, self.side, self.symbol, with_mark_price=True) + self._update_margin() def update_average_entry_price(self, update_size, update_price): """ diff --git a/tests/modes/script_keywords/basic_keywords/test_amount.py b/tests/modes/script_keywords/basic_keywords/test_amount.py index 59689e4357..1f46e18a2b 100644 --- a/tests/modes/script_keywords/basic_keywords/test_amount.py +++ b/tests/modes/script_keywords/basic_keywords/test_amount.py @@ -24,6 +24,7 @@ import octobot_trading.modes.script_keywords as script_keywords import octobot_trading.modes.script_keywords.dsl as dsl import octobot_trading.modes.script_keywords.basic_keywords.account_balance as account_balance +import octobot_trading.modes.script_keywords.basic_keywords.position as position_kw from tests import event_loop from tests.modes.script_keywords import null_context @@ -243,3 +244,44 @@ async def test_get_amount_from_input_amount(null_context): parse_quantity_mock.assert_called_once_with("50") get_holdings_value_mock.assert_called_once_with({'BTC', 'ETH', 'SOL', 'USDT'}, "BTC") adapt_amount_to_holdings_mock.reset_mock() + + +async def test_get_amount_from_input_amount_for_position(null_context): + null_context.exchange_manager = mock.Mock( + exchange_personal_data=mock.Mock( + portfolio_manager=mock.Mock( + portfolio_value_holder=mock.Mock() + ) + ) + ) + null_context.exchange_manager.is_future = False + with pytest.raises(NotImplementedError): + # not futures + await script_keywords.get_amount_from_input_amount( + null_context, f"1{script_keywords.QuantityType.POSITION_PERCENT.value}" + ) + + null_context.exchange_manager.is_future = True + + # not one-way + with mock.patch.object( + position_kw, "is_in_one_way_position_mode", mock.Mock(return_value=False) + ) as is_in_one_way_position_mode_mock: + with pytest.raises(NotImplementedError): + await script_keywords.get_amount_from_input_amount( + null_context, f"1{script_keywords.QuantityType.POSITION_PERCENT.value}" + ) + is_in_one_way_position_mode_mock.assert_called_once_with(null_context) + + # futures one-way: works + with (mock.patch.object(position_kw, "is_in_one_way_position_mode", mock.Mock(return_value=True)) + as is_in_one_way_position_mode_mock, + mock.patch.object(position_kw, "get_position", mock.Mock(return_value=mock.Mock(size=decimal.Decimal("100")))) + as get_position_mock, mock.patch.object(account_balance, "adapt_amount_to_holdings", + mock.AsyncMock(return_value=decimal.Decimal(1))) as adapt_amount_to_holdings_mock): + assert await script_keywords.get_amount_from_input_amount( + null_context, f"1{script_keywords.QuantityType.POSITION_PERCENT.value}" + ) == decimal.Decimal(1) + is_in_one_way_position_mode_mock.assert_called_once_with(null_context) + get_position_mock.assert_called_once() + adapt_amount_to_holdings_mock.assert_called_once() diff --git a/tests/personal_data/positions/test_position.py b/tests/personal_data/positions/test_position.py index 54c82a5454..a7a5918fb2 100644 --- a/tests/personal_data/positions/test_position.py +++ b/tests/personal_data/positions/test_position.py @@ -82,11 +82,13 @@ async def test_update_margin_linear(btc_usdt_future_trader_simulator_with_defaul await position_inst.update(mark_price=constants.ONE, update_margin=decimal.Decimal(5)) assert position_inst.mark_price == constants.ONE assert position_inst.initial_margin == decimal.Decimal(5) + assert position_inst.margin == decimal.Decimal("5.0300") # initial margin + closing fees assert position_inst.size == decimal.Decimal(50) await position_inst.update(mark_price=constants.ONE, update_margin=-decimal.Decimal(3)) assert position_inst.mark_price == constants.ONE assert position_inst.initial_margin == decimal.Decimal(2) + assert position_inst.margin == decimal.Decimal("2.0120") assert position_inst.size == decimal.Decimal(20) @@ -103,11 +105,13 @@ async def test_update_margin_inverse(btc_usdt_future_trader_simulator_with_defau await position_inst.update(mark_price=constants.ONE, update_margin=decimal.Decimal(8) / constants.ONE_HUNDRED) assert position_inst.mark_price == constants.ONE assert position_inst.initial_margin == decimal.Decimal('0.08') + assert position_inst.margin == decimal.Decimal("0.080288") # initial margin + closing fees assert position_inst.size == decimal.Decimal('0.4') await position_inst.update(mark_price=constants.ONE, update_margin=-constants.ONE / constants.ONE_HUNDRED) assert position_inst.mark_price == constants.ONE assert position_inst.initial_margin == decimal.Decimal('0.07') + assert position_inst.margin == decimal.Decimal("0.070252") assert position_inst.size == decimal.Decimal('0.35') diff --git a/tests/personal_data/positions/types/test_inverse_position.py b/tests/personal_data/positions/types/test_inverse_position.py index f2062c8f88..9f6cf355a6 100644 --- a/tests/personal_data/positions/types/test_inverse_position.py +++ b/tests/personal_data/positions/types/test_inverse_position.py @@ -217,13 +217,11 @@ async def test_get_bankruptcy_price_with_long(future_trader_simulator_with_defau assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal(50) default_contract.set_current_leverage(constants.ONE_HUNDRED) assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side) == decimal.Decimal("99.00990099009900990099009901") - assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("0.9900990099009900990099009901") + assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal('99.00990099009900990099009901') await position_inst.update(update_size=constants.ONE_HUNDRED, mark_price=decimal.Decimal(2) * constants.ONE_HUNDRED) assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side) == decimal.Decimal("99.00990099009900990099009901") - assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("1.980198019801980198019801980") - assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == \ - decimal.Decimal("1.980198019801980198019801980") + assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("198.0198019801980198019801980") assert position_inst.get_bankruptcy_price(decimal.Decimal("200"), position_inst.side) == decimal.Decimal("198.0198019801980198019801980") assert position_inst.get_bankruptcy_price(decimal.Decimal("200"), enums.PositionSide.SHORT) \ == decimal.Decimal("202.0202020202020202020202020") @@ -239,7 +237,7 @@ async def test_get_bankruptcy_price_with_short(future_trader_simulator_with_defa assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == constants.ZERO default_contract.set_current_leverage(constants.ONE_HUNDRED) assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side) == decimal.Decimal("101.0101010101010101010101010") - assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("1.010101010101010101010101010") + assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("101.0101010101010101010101010") await position_inst.update(update_size=constants.ONE_HUNDRED, mark_price=decimal.Decimal(2) * constants.ONE_HUNDRED) assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side) == constants.ZERO @@ -248,7 +246,7 @@ async def test_get_bankruptcy_price_with_short(future_trader_simulator_with_defa position_inst.entry_price = constants.ONE_HUNDRED await position_inst.update(update_size=-constants.ONE_HUNDRED, mark_price=constants.ONE_HUNDRED) assert position_inst.get_bankruptcy_price(decimal.Decimal("20"), position_inst.side, with_mark_price=True) == \ - decimal.Decimal("16.66666666666666666666666667") + decimal.Decimal("116.6666666666666666666666667") assert position_inst.get_bankruptcy_price(decimal.Decimal("100"), position_inst.side) == \ decimal.Decimal("116.6666666666666666666666667") assert position_inst.get_bankruptcy_price(decimal.Decimal("100"), enums.PositionSide.LONG) \