1515# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
1616import contextlib
1717import decimal
18+ import pytest
1819
1920import octobot_trading.enums as trading_enums
2021import octobot_trading.constants as trading_constants
22+ import octobot_trading.errors as trading_errors
2123from exchanges_tests import abstract_authenticated_exchange_tester
2224
2325
@@ -27,9 +29,11 @@ class AbstractAuthenticatedFutureExchangeTester(
2729 # enter exchange name as a class variable here*
2830 EXCHANGE_TYPE = trading_enums.ExchangeTypes.FUTURE.value
2931 PORTFOLIO_TYPE_FOR_SIZE = trading_constants.CONFIG_PORTFOLIO_TOTAL
30- REQUIRES_SYMBOLS_TO_GET_POSITIONS = False
3132 INVERSE_SYMBOL = None
3233 MIN_PORTFOLIO_SIZE = 2 # ensure fetching currency for linear and inverse
34+ SUPPORTS_GET_LEVERAGE = True
35+ SUPPORTS_SET_LEVERAGE = True
36+ SUPPORTS_EMPTY_POSITION_SET_MARGIN_TYPE = True
3337
3438 async def test_get_empty_linear_and_inverse_positions(self):
3539 # ensure fetch empty positions
@@ -38,19 +42,103 @@ async def test_get_empty_linear_and_inverse_positions(self):
3842
3943 async def inner_test_get_empty_linear_and_inverse_positions(self):
4044 positions = await self.get_positions()
41- self._check_position_content(positions)
45+ self._check_positions_content(positions)
46+ position = await self.get_position(self.SYMBOL)
47+ self._check_position_content(position, self.SYMBOL)
4248 for contract_type in (trading_enums.FutureContractType.LINEAR_PERPETUAL,
4349 trading_enums.FutureContractType.INVERSE_PERPETUAL):
4450 if not self.has_empty_position(self.get_filtered_positions(positions, contract_type)):
4551 empty_position_symbol = self.get_other_position_symbol(positions, contract_type)
52+ # test with get_position
4653 empty_position = await self.get_position(empty_position_symbol)
4754 assert self.is_position_empty(empty_position)
55+ # test with get_positions
56+ empty_positions = await self.get_positions([empty_position_symbol])
57+ assert len(empty_positions) == 1
58+ assert self.is_position_empty(empty_positions[0])
4859
49- def _check_position_content(self, positions):
60+ async def test_get_and_set_leverage(self):
61+ # ensure set_leverage works
62+ async with self.local_exchange_manager():
63+ await self.inner_test_get_and_set_leverage()
64+
65+ async def inner_test_get_and_set_leverage(self):
66+ contract = await self.init_and_get_contract()
67+ origin_margin_type = contract.margin_type
68+ origin_leverage = contract.current_leverage
69+ assert origin_leverage != trading_constants.ZERO
70+ if self.SUPPORTS_GET_LEVERAGE:
71+ assert origin_leverage == await self.get_leverage()
72+ if not self.SUPPORTS_SET_LEVERAGE:
73+ return
74+ new_leverage = origin_leverage + 1
75+ await self.set_leverage(new_leverage)
76+ await self._check_margin_type_and_leverage(origin_margin_type, new_leverage) # did not change margin type
77+ # change leverage back to origin value
78+ await self.set_leverage(origin_leverage)
79+ await self._check_margin_type_and_leverage(origin_margin_type, origin_leverage) # did not change margin type
80+
81+ async def test_get_and_set_margin_type(self):
82+ # ensure set_leverage works
83+ async with self.local_exchange_manager():
84+ await self.inner_test_get_and_set_margin_type(allow_empty_position=True)
85+
86+ async def inner_test_get_and_set_margin_type(self, allow_empty_position=False, symbol=None):
87+ contract = await self.init_and_get_contract(symbol=symbol)
88+ origin_margin_type = contract.margin_type
89+ origin_leverage = contract.current_leverage
90+ new_margin_type = trading_enums.MarginType.CROSS \
91+ if origin_margin_type is trading_enums.MarginType.ISOLATED else trading_enums.MarginType.ISOLATED
92+ if not self.exchange_manager.exchange.SUPPORTS_SET_MARGIN_TYPE:
93+ assert origin_margin_type in (trading_enums.MarginType.ISOLATED, trading_enums.MarginType.CROSS)
94+ with pytest.raises(AttributeError):
95+ await self.exchange_manager.exchange.connector.set_symbol_margin_type(symbol, True)
96+ with pytest.raises(trading_errors.NotSupported):
97+ await self.set_margin_type(new_margin_type, symbol=symbol)
98+ return
99+ await self.set_margin_type(new_margin_type, symbol=symbol)
100+ position = await self.get_position(symbol=symbol)
101+ if allow_empty_position and (
102+ position[trading_enums.ExchangeConstantsPositionColumns.SIZE.value] != trading_constants.ZERO
103+ or self.SUPPORTS_EMPTY_POSITION_SET_MARGIN_TYPE
104+ ):
105+ # did not change leverage
106+ await self._check_margin_type_and_leverage(new_margin_type, origin_leverage, symbol=symbol)
107+ # restore margin type
108+ await self.set_margin_type(origin_margin_type, symbol=symbol)
109+ # did not change leverage
110+ await self._check_margin_type_and_leverage(origin_margin_type, origin_leverage, symbol=symbol)
111+
112+ async def set_margin_type(self, margin_type, symbol=None):
113+ await self.exchange_manager.exchange.set_symbol_margin_type(
114+ symbol or self.SYMBOL,
115+ margin_type is trading_enums.MarginType.ISOLATED
116+ )
117+
118+ async def _check_margin_type_and_leverage(self, expected_margin_type, expected_leverage, symbol=None):
119+ margin_type, leverage = await self.get_margin_type_and_leverage_from_position(symbol=symbol)
120+ assert expected_margin_type is margin_type
121+ assert expected_leverage == leverage
122+ if self.SUPPORTS_GET_LEVERAGE:
123+ assert expected_leverage == await self.get_leverage(symbol=symbol)
124+
125+ def _check_positions_content(self, positions):
50126 for position in positions:
127+ self._check_position_content(position, None)
128+
129+ def _check_position_content(self, position, symbol, position_mode=None):
130+ if symbol:
131+ assert position[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value] == symbol
132+ else:
51133 assert position[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value]
52- # should not be 0 in octobot
53- assert position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value] > 0
134+ leverage = position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value]
135+ assert isinstance(leverage, decimal.Decimal)
136+ # should not be 0 in octobot
137+ assert leverage > 0
138+ assert position[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value] is not None
139+ assert position[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] is not None
140+ if position_mode is not None:
141+ assert position[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] is position_mode
54142
55143 async def inner_test_create_and_fill_market_orders(self):
56144 portfolio = await self.get_portfolio()
@@ -62,39 +150,64 @@ async def inner_test_create_and_fill_market_orders(self):
62150 # buy: increase position
63151 buy_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.BUY)
64152 self.check_created_market_order(buy_market, size, trading_enums.TradeOrderSide.BUY)
65- await self.wait_for_fill(buy_market)
66- post_buy_portfolio = await self.get_portfolio()
67- post_buy_position = await self.get_position()
68- self.check_portfolio_changed(portfolio, post_buy_portfolio, False)
69- self.check_position_changed(position, post_buy_position, True)
70- post_order_positions = await self.get_positions()
71- self.check_position_in_positions(pre_order_positions + post_order_positions)
72- # sell: reset portfolio & position
73- sell_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.SELL)
74- self.check_created_market_order(sell_market, size, trading_enums.TradeOrderSide.SELL)
75- await self.wait_for_fill(sell_market)
76- post_sell_portfolio = await self.get_portfolio()
77- post_sell_position = await self.get_position()
78- self.check_portfolio_changed(post_buy_portfolio, post_sell_portfolio, True)
79- self.check_position_changed(post_buy_position, post_sell_position, False)
80- # position is back to what it was at the beginning on the test
81- self.check_position_size(position, post_sell_position)
82-
83- async def test_create_bundled_orders(self):
84- async with self.local_exchange_manager(), self.required_empty_position():
85- await self.inner_test_create_bundled_orders()
153+ post_buy_portfolio = {}
154+ post_buy_position = None
155+ try:
156+ await self.wait_for_fill(buy_market)
157+ post_buy_portfolio = await self.get_portfolio()
158+ post_buy_position = await self.get_position()
159+ self._check_position_content(post_buy_position, self.SYMBOL,
160+ position_mode=trading_enums.PositionMode.ONE_WAY)
161+ self.check_portfolio_changed(portfolio, post_buy_portfolio, False)
162+ self.check_position_changed(position, post_buy_position, True)
163+ post_order_positions = await self.get_positions()
164+ self.check_position_in_positions(pre_order_positions + post_order_positions)
165+ # now that position is open, test margin type update
166+ await self.inner_test_get_and_set_margin_type()
167+ finally:
168+ # sell: reset portfolio & position
169+ sell_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.SELL)
170+ self.check_created_market_order(sell_market, size, trading_enums.TradeOrderSide.SELL)
171+ await self.wait_for_fill(sell_market)
172+ post_sell_portfolio = await self.get_portfolio()
173+ post_sell_position = await self.get_position()
174+ self.check_portfolio_changed(post_buy_portfolio, post_sell_portfolio, True)
175+ self.check_position_changed(post_buy_position, post_sell_position, False)
176+ # position is back to what it was at the beginning on the test
177+ self.check_position_size(position, post_sell_position)
86178
87179 async def get_position(self, symbol=None):
88180 return await self.exchange_manager.exchange.get_position(symbol or self.SYMBOL)
89181
90- async def get_positions(self):
91- symbols = None
92- if self.REQUIRES_SYMBOLS_TO_GET_POSITIONS :
182+ async def get_positions(self, symbols=None ):
183+ symbols = symbols or None
184+ if symbols is None and self.exchange_manager.exchange.REQUIRES_SYMBOL_FOR_EMPTY_POSITION :
93185 if self.INVERSE_SYMBOL is None:
94186 raise AssertionError(f"INVERSE_SYMBOL is required")
95187 symbols = [self.SYMBOL, self.INVERSE_SYMBOL]
96188 return await self.exchange_manager.exchange.get_positions(symbols=symbols)
97189
190+ async def init_and_get_contract(self, symbol=None):
191+ symbol = symbol or self.SYMBOL
192+ await self.exchange_manager.exchange.load_pair_future_contract(symbol)
193+ if not self.exchange_manager.exchange.has_pair_future_contract(symbol):
194+ raise AssertionError(f"{symbol} contract not initialized")
195+ return self.exchange_manager.exchange.get_pair_future_contract(symbol)
196+
197+ async def get_margin_type_and_leverage_from_position(self, symbol=None):
198+ position = await self.get_position(symbol=symbol)
199+ return (
200+ position[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value],
201+ position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value],
202+ )
203+
204+ async def get_leverage(self, symbol=None):
205+ leverage = await self.exchange_manager.exchange.get_symbol_leverage(symbol or self.SYMBOL)
206+ return leverage[trading_enums.ExchangeConstantsLeveragePropertyColumns.LEVERAGE.value]
207+
208+ async def set_leverage(self, leverage, symbol=None):
209+ return await self.exchange_manager.exchange.set_symbol_leverage(symbol or self.SYMBOL, float(leverage))
210+
98211 @contextlib.asynccontextmanager
99212 async def required_empty_position(self):
100213 position = await self.get_position()
@@ -168,7 +281,7 @@ def get_other_position_symbol(self, positions_blacklist, contract_type):
168281 for position in positions_blacklist
169282 )
170283 for symbol in self.exchange_manager.exchange.connector.client.markets:
171- if symbol in ignored_symbols:
284+ if symbol in ignored_symbols or self.exchange_manager.exchange.is_expirable_symbol(symbol) :
172285 continue
173286 if contract_type is trading_enums.FutureContractType.INVERSE_PERPETUAL \
174287 and self.exchange_manager.exchange.is_inverse_symbol(symbol):
@@ -179,6 +292,10 @@ def get_other_position_symbol(self, positions_blacklist, contract_type):
179292 raise AssertionError(f"No free symbol for {contract_type}")
180293
181294 def is_position_empty(self, position):
295+ if position is None:
296+ raise AssertionError(
297+ f"Fetched empty position should never be None as a symbol parameter is given"
298+ )
182299 return position[trading_enums.ExchangeConstantsPositionColumns.SIZE.value] == trading_constants.ZERO
183300
184301 def check_position_in_positions(self, positions, symbol=None):
@@ -204,4 +321,8 @@ def check_theoretical_cost(self, symbol, quantity, price, cost):
204321 assert theoretical_cost * decimal.Decimal("0.8") <= cost <= theoretical_cost * decimal.Decimal("1.2")
205322
206323 def _get_all_symbols(self):
207- return [self.SYMBOL, self.INVERSE_SYMBOL]
324+ return [
325+ symbol
326+ for symbol in (self.SYMBOL, self.INVERSE_SYMBOL)
327+ if symbol
328+ ]
0 commit comments