Skip to content

Commit 893f561

Browse files
committed
[Futures] fix margin computation and tests
1 parent 14de219 commit 893f561

File tree

6 files changed

+64
-16
lines changed

6 files changed

+64
-16
lines changed

octobot_trading/personal_data/positions/position.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ def _update(self, position_id, exchange_position_id, symbol, currency, market, t
209209
# update side after quantity as it relies on self.quantity
210210
self._update_side(not entry_price)
211211
self._update_prices_if_necessary(mark_price)
212+
if changed:
213+
# ensure fee to close and margin are up to date now that all other attributes are set
214+
self.update_fee_to_close()
215+
self._update_margin()
212216
return changed
213217

214218
async def ensure_position_initialized(self, **kwargs):

octobot_trading/personal_data/positions/types/inverse_position.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,17 @@ def get_bankruptcy_price(self, price, side, with_mark_price=False):
102102
Short position = (Entry Price x Leverage) / (Leverage - 1)
103103
"""
104104
try:
105+
price = self.mark_price if with_mark_price else price
105106
if side is enums.PositionSide.LONG:
106-
return (self.mark_price if with_mark_price else
107-
price * self.symbol_contract.current_leverage) \
107+
return (
108+
price * self.symbol_contract.current_leverage
108109
/ (self.symbol_contract.current_leverage + constants.ONE)
110+
)
109111
elif side is enums.PositionSide.SHORT:
110-
return (self.mark_price if with_mark_price else
111-
price * self.symbol_contract.current_leverage) \
112+
return (
113+
price * self.symbol_contract.current_leverage
112114
/ (self.symbol_contract.current_leverage - constants.ONE)
115+
)
113116
return constants.ZERO
114117
except (decimal.DivisionByZero, decimal.InvalidOperation):
115118
return constants.ZERO
@@ -134,7 +137,7 @@ def get_fee_to_close(self, quantity, price, side, symbol, with_mark_price=False)
134137
:return: Fee to open = (Quantity * Mark Price) x Taker fee
135138
"""
136139
try:
137-
return quantity / \
140+
return abs(quantity) / \
138141
self.get_bankruptcy_price(price, side, with_mark_price=with_mark_price) * self.get_taker_fee(symbol)
139142
except (decimal.DivisionByZero, decimal.InvalidOperation):
140143
return constants.ZERO

octobot_trading/personal_data/positions/types/linear_position.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def get_fee_to_close(self, quantity, price, side, symbol, with_mark_price=False)
109109
"""
110110
:return: Fee to open = (Quantity * Mark Price) x Taker fee
111111
"""
112-
return quantity * self.get_bankruptcy_price(price, side, with_mark_price=with_mark_price) * \
112+
return abs(quantity) * self.get_bankruptcy_price(price, side, with_mark_price=with_mark_price) * \
113113
self.get_taker_fee(symbol)
114114

115115
def get_order_cost(self):
@@ -124,6 +124,7 @@ def update_fee_to_close(self):
124124
"""
125125
self.fee_to_close = self.get_fee_to_close(self.size, self.entry_price, self.side, self.symbol,
126126
with_mark_price=True)
127+
self._update_margin()
127128

128129
def update_average_entry_price(self, update_size, update_price):
129130
"""

tests/modes/script_keywords/basic_keywords/test_amount.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import octobot_trading.modes.script_keywords as script_keywords
2525
import octobot_trading.modes.script_keywords.dsl as dsl
2626
import octobot_trading.modes.script_keywords.basic_keywords.account_balance as account_balance
27+
import octobot_trading.modes.script_keywords.basic_keywords.position as position_kw
2728

2829
from tests import event_loop
2930
from tests.modes.script_keywords import null_context
@@ -46,10 +47,6 @@ async def test_get_amount_from_input_amount(null_context):
4647
with pytest.raises(errors.InvalidArgumentError):
4748
await script_keywords.get_amount_from_input_amount(null_context, "1sdsqdq")
4849

49-
with pytest.raises(NotImplementedError):
50-
await script_keywords.get_amount_from_input_amount(null_context,
51-
f"1{script_keywords.QuantityType.POSITION_PERCENT.value}")
52-
5350
with mock.patch.object(account_balance, "adapt_amount_to_holdings",
5451
mock.AsyncMock(return_value=decimal.Decimal(1))) as adapt_amount_to_holdings_mock:
5552
for quantity_delta in (script_keywords.QuantityType.DELTA, script_keywords.QuantityType.DELTA_BASE):
@@ -243,3 +240,44 @@ async def test_get_amount_from_input_amount(null_context):
243240
parse_quantity_mock.assert_called_once_with("50")
244241
get_holdings_value_mock.assert_called_once_with({'BTC', 'ETH', 'SOL', 'USDT'}, "BTC")
245242
adapt_amount_to_holdings_mock.reset_mock()
243+
244+
245+
async def test_get_amount_from_input_amount_for_position(null_context):
246+
null_context.exchange_manager = mock.Mock(
247+
exchange_personal_data=mock.Mock(
248+
portfolio_manager=mock.Mock(
249+
portfolio_value_holder=mock.Mock()
250+
)
251+
)
252+
)
253+
null_context.exchange_manager.is_future = False
254+
with pytest.raises(NotImplementedError):
255+
# not futures
256+
await script_keywords.get_amount_from_input_amount(
257+
null_context, f"1{script_keywords.QuantityType.POSITION_PERCENT.value}"
258+
)
259+
260+
null_context.exchange_manager.is_future = True
261+
262+
# not one-way
263+
with mock.patch.object(
264+
position_kw, "is_in_one_way_position_mode", mock.Mock(return_value=False)
265+
) as is_in_one_way_position_mode_mock:
266+
with pytest.raises(NotImplementedError):
267+
await script_keywords.get_amount_from_input_amount(
268+
null_context, f"1{script_keywords.QuantityType.POSITION_PERCENT.value}"
269+
)
270+
is_in_one_way_position_mode_mock.assert_called_once_with(null_context)
271+
272+
# futures one-way: works
273+
with (mock.patch.object(position_kw, "is_in_one_way_position_mode", mock.Mock(return_value=True))
274+
as is_in_one_way_position_mode_mock,
275+
mock.patch.object(position_kw, "get_position", mock.Mock(return_value=mock.Mock(size=decimal.Decimal("100"))))
276+
as get_position_mock, mock.patch.object(account_balance, "adapt_amount_to_holdings",
277+
mock.AsyncMock(return_value=decimal.Decimal(1))) as adapt_amount_to_holdings_mock):
278+
assert await script_keywords.get_amount_from_input_amount(
279+
null_context, f"1{script_keywords.QuantityType.POSITION_PERCENT.value}"
280+
) == decimal.Decimal(1)
281+
is_in_one_way_position_mode_mock.assert_called_once_with(null_context)
282+
get_position_mock.assert_called_once()
283+
adapt_amount_to_holdings_mock.assert_called_once()

tests/personal_data/positions/test_position.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,13 @@ async def test_update_margin_linear(btc_usdt_future_trader_simulator_with_defaul
8282
await position_inst.update(mark_price=constants.ONE, update_margin=decimal.Decimal(5))
8383
assert position_inst.mark_price == constants.ONE
8484
assert position_inst.initial_margin == decimal.Decimal(5)
85+
assert position_inst.margin == decimal.Decimal("5.0300") # initial margin + closing fees
8586
assert position_inst.size == decimal.Decimal(50)
8687

8788
await position_inst.update(mark_price=constants.ONE, update_margin=-decimal.Decimal(3))
8889
assert position_inst.mark_price == constants.ONE
8990
assert position_inst.initial_margin == decimal.Decimal(2)
91+
assert position_inst.margin == decimal.Decimal("2.0120")
9092
assert position_inst.size == decimal.Decimal(20)
9193

9294

@@ -103,11 +105,13 @@ async def test_update_margin_inverse(btc_usdt_future_trader_simulator_with_defau
103105
await position_inst.update(mark_price=constants.ONE, update_margin=decimal.Decimal(8) / constants.ONE_HUNDRED)
104106
assert position_inst.mark_price == constants.ONE
105107
assert position_inst.initial_margin == decimal.Decimal('0.08')
108+
assert position_inst.margin == decimal.Decimal("0.080288") # initial margin + closing fees
106109
assert position_inst.size == decimal.Decimal('0.4')
107110

108111
await position_inst.update(mark_price=constants.ONE, update_margin=-constants.ONE / constants.ONE_HUNDRED)
109112
assert position_inst.mark_price == constants.ONE
110113
assert position_inst.initial_margin == decimal.Decimal('0.07')
114+
assert position_inst.margin == decimal.Decimal("0.070252")
111115
assert position_inst.size == decimal.Decimal('0.35')
112116

113117

tests/personal_data/positions/types/test_inverse_position.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,11 @@ async def test_get_bankruptcy_price_with_long(future_trader_simulator_with_defau
217217
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal(50)
218218
default_contract.set_current_leverage(constants.ONE_HUNDRED)
219219
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side) == decimal.Decimal("99.00990099009900990099009901")
220-
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("0.9900990099009900990099009901")
220+
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal('99.00990099009900990099009901')
221221
await position_inst.update(update_size=constants.ONE_HUNDRED,
222222
mark_price=decimal.Decimal(2) * constants.ONE_HUNDRED)
223223
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side) == decimal.Decimal("99.00990099009900990099009901")
224-
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("1.980198019801980198019801980")
225-
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == \
226-
decimal.Decimal("1.980198019801980198019801980")
224+
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("198.0198019801980198019801980")
227225
assert position_inst.get_bankruptcy_price(decimal.Decimal("200"), position_inst.side) == decimal.Decimal("198.0198019801980198019801980")
228226
assert position_inst.get_bankruptcy_price(decimal.Decimal("200"), enums.PositionSide.SHORT) \
229227
== decimal.Decimal("202.0202020202020202020202020")
@@ -239,7 +237,7 @@ async def test_get_bankruptcy_price_with_short(future_trader_simulator_with_defa
239237
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == constants.ZERO
240238
default_contract.set_current_leverage(constants.ONE_HUNDRED)
241239
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side) == decimal.Decimal("101.0101010101010101010101010")
242-
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("1.010101010101010101010101010")
240+
assert position_inst.get_bankruptcy_price(position_inst.entry_price, position_inst.side, with_mark_price=True) == decimal.Decimal("101.0101010101010101010101010")
243241
await position_inst.update(update_size=constants.ONE_HUNDRED,
244242
mark_price=decimal.Decimal(2) * constants.ONE_HUNDRED)
245243
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
248246
position_inst.entry_price = constants.ONE_HUNDRED
249247
await position_inst.update(update_size=-constants.ONE_HUNDRED, mark_price=constants.ONE_HUNDRED)
250248
assert position_inst.get_bankruptcy_price(decimal.Decimal("20"), position_inst.side, with_mark_price=True) == \
251-
decimal.Decimal("16.66666666666666666666666667")
249+
decimal.Decimal("116.6666666666666666666666667")
252250
assert position_inst.get_bankruptcy_price(decimal.Decimal("100"), position_inst.side) == \
253251
decimal.Decimal("116.6666666666666666666666667")
254252
assert position_inst.get_bankruptcy_price(decimal.Decimal("100"), enums.PositionSide.LONG) \

0 commit comments

Comments
 (0)