Skip to content

Commit

Permalink
[Futures] fix margin computation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeDSM committed Nov 28, 2024
1 parent 14de219 commit 4abe237
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 12 deletions.
4 changes: 4 additions & 0 deletions octobot_trading/personal_data/positions/position.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
"""
Expand Down
42 changes: 42 additions & 0 deletions tests/modes/script_keywords/basic_keywords/test_amount.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
4 changes: 4 additions & 0 deletions tests/personal_data/positions/test_position.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand All @@ -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')


Expand Down
10 changes: 4 additions & 6 deletions tests/personal_data/positions/types/test_inverse_position.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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) \
Expand Down

0 comments on commit 4abe237

Please sign in to comment.