def test_create_profit_taking_proposal_logs_when_one_way_mode_and_multiple_positions(self): positions = [ Position( trading_pair=(self.trading_pair), position_side=PositionSide.LONG, unrealized_pnl=Decimal(1000), entry_price=Decimal(50000), amount=Decimal(1), leverage=Decimal(10) ), Position( trading_pair=(self.trading_pair), position_side=PositionSide.SHORT, unrealized_pnl=Decimal(1000), entry_price=Decimal(50000), amount=Decimal(1), leverage=Decimal(10) )] self.strategy.profit_taking_proposal(PositionMode.ONEWAY, positions) self.assertTrue( self._is_logged( "ERROR", "More than one open position in ONEWAY position mode. " "Kindly ensure you do not interact with the exchange through other platforms and" " restart this strategy."))
def test_strategy_starts_with_multiple_active_position(self): # arbitrary adding multiple actuve positions here, which should never happen in real trading on oneway mode self.strategy._position_mode_ready = True self.perp_connector._account_positions[ trading_pair + "SHORT"] = Position( trading_pair, PositionSide.SHORT, Decimal("0"), Decimal("95"), Decimal("-1"), self.perp_connector.get_leverage(trading_pair)) self.perp_connector._account_positions[ trading_pair + "LONG"] = Position( trading_pair, PositionSide.LONG, Decimal("0"), Decimal("95"), Decimal("1"), self.perp_connector.get_leverage(trading_pair)) self.clock.add_iterator(self.strategy) self.clock.backtest_til(self.start_timestamp + 2) self.assertTrue(self._is_logged("INFO", "Markets are ready.")) self.assertTrue(self._is_logged("INFO", "Trading started."))
def test_status_text_with_one_open_position_and_no_orders_alive(self): position = Position(trading_pair=(self.trading_pair), position_side=PositionSide.LONG, unrealized_pnl=Decimal(1000), entry_price=self.market.get_price( self.trading_pair, True), amount=Decimal(1), leverage=Decimal(10)) self.market.account_positions[self.trading_pair] = position self.clock.backtest_til(self.start_timestamp + 1) expected_status = ( "\n Markets:" "\n Exchange Market Best Bid Best Ask Ref Price (MidPrice)" "\n MockPerpConnector COINALPHA-HBOT 99.5 100.5 100" "\n\n Assets:" "\n HBOT" "\n Total Balance 50000" "\n Available Balance 50000" "\n\n No active maker orders." "\n\n Positions:" "\n Symbol Type Entry Price Amount Leverage Unrealized PnL" "\n COINALPHA-HBOT LONG 100.50 1 10 0.00" ) status = self.strategy.format_status() self.assertEqual(expected_status, status)
def test_create_profit_taking_proposal_for_short_position_cancel_old_exit_orders( self): order_id = self.strategy.buy_with_specific_market( market_trading_pair_tuple=self.market_info, amount=Decimal(1), order_type=OrderType.LIMIT, price=Decimal(self.initial_mid_price)) self.strategy._exit_orders[order_id] = 1000 position = Position(trading_pair=(self.trading_pair), position_side=PositionSide.LONG, unrealized_pnl=Decimal(1000), entry_price=self.initial_mid_price + Decimal(20), amount=Decimal(-1), leverage=Decimal(10)) positions = [position] self.market.set_balanced_order_book(trading_pair=self.trading_pair, mid_price=self.initial_mid_price + 10, min_price=1, max_price=200, price_step_size=1, volume_step_size=10) self.strategy.profit_taking_proposal(PositionMode.ONEWAY, positions) self.assertEqual(order_id, self.cancel_order_logger.event_log[0].order_id) self.assertTrue( self._is_logged( "INFO", f"Initiated cancellation of previous take profit order {order_id} " f"in favour of new take profit order.")) self.assertEqual(0, len(self.strategy.active_orders))
def test_create_profit_taking_proposal_for_short_position(self): position = Position(trading_pair=(self.trading_pair), position_side=PositionSide.LONG, unrealized_pnl=Decimal(1000), entry_price=self.initial_mid_price + Decimal(20), amount=Decimal(-1), leverage=Decimal(10)) positions = [position] self.market.set_balanced_order_book(trading_pair=self.trading_pair, mid_price=self.initial_mid_price - 10, min_price=1, max_price=200, price_step_size=1, volume_step_size=10) close_proposal = self.strategy.profit_taking_proposal( PositionMode.ONEWAY, positions) self.assertEqual(0, len(close_proposal.sells)) self.assertEqual( position.entry_price * (Decimal(1) - self.short_profit_taking_spread), close_proposal.buys[0].price) self.assertEqual(Decimal("1"), close_proposal.buys[0].size)
def test_check_budget_constraint(self): proposal = ArbProposal(ArbProposalSide(self.spot_market_info, False, Decimal("100")), ArbProposalSide(self.perp_market_info, True, Decimal("100")), Decimal("1")) self.spot_connector.set_balance(base_asset, 0.5) self.spot_connector.set_balance(quote_asset, 0) self.perp_connector.set_balance(base_asset, 0) self.perp_connector.set_balance(quote_asset, 21) # Since spot has 0.5 HBOT, not enough to sell on 1 order amount self.assertFalse(self.strategy.check_budget_constraint(proposal)) self.spot_connector.set_balance(base_asset, 1) self.assertTrue(self.strategy.check_budget_constraint(proposal)) # on perpetual you need at least 100/5 to open a position self.perp_connector.set_balance(quote_asset, 10) self.assertFalse(self.strategy.check_budget_constraint(proposal)) # There is no balance required to close a position self.perp_connector._account_positions[trading_pair] = Position( trading_pair, PositionSide.SHORT, Decimal("0"), Decimal("95"), Decimal("-1"), self.perp_connector.get_leverage(trading_pair) ) self.assertTrue(self.strategy.check_budget_constraint(proposal))
def test_strategy_starts_with_existing_position_unmatched_pos_amount(self): """ Tests if the strategy start then stop when there is an existing position where position amount doesn't match strategy order amount """ self.clock.add_iterator(self.strategy) self.perp_connector._account_positions[trading_pair] = Position( trading_pair, PositionSide.SHORT, Decimal("0"), Decimal("95"), Decimal("-10"), self.perp_connector.get_leverage(trading_pair) ) self.clock.backtest_til(self.start_timestamp + 1) self.assertTrue(self._is_logged("INFO", "Markets are ready.")) self.assertTrue(self._is_logged("INFO", "Trading started.")) self.assertTrue(self._is_logged("INFO", f"There is an existing {trading_pair} " f"{PositionSide.SHORT.name} position with unmatched position amount. " f"Please manually close out the position before starting this " f"strategy.")) asyncio.get_event_loop().run_until_complete(asyncio.sleep(0.01)) self.clock.backtest_til(self.start_timestamp + 2) # assert the strategy stopped here self.assertIsNone(self.strategy.clock)
def test_create_profit_taking_proposal_for_one_way_cancels_other_possible_exit_orders( self): order_id = self.strategy.buy_with_specific_market( market_trading_pair_tuple=self.market_info, amount=Decimal(1), order_type=OrderType.LIMIT, price=Decimal(50000)) positions = [ Position(trading_pair=(self.trading_pair), position_side=PositionSide.LONG, unrealized_pnl=Decimal(1000), entry_price=Decimal(50000), amount=Decimal(-1), leverage=Decimal(10)) ] self.strategy.profit_taking_proposal(PositionMode.ONEWAY, positions) self.assertEqual(0, len(self.market.limit_orders)) self.assertTrue( self._is_logged( "INFO", f"Initiated cancellation of buy order {order_id} in favour of take profit order." )) order_id = self.strategy.sell_with_specific_market( market_trading_pair_tuple=self.market_info, amount=Decimal(1), order_type=OrderType.LIMIT, price=Decimal(50000)) positions = [ Position(trading_pair=(self.trading_pair), position_side=PositionSide.LONG, unrealized_pnl=Decimal(1000), entry_price=Decimal(50000), amount=Decimal(1), leverage=Decimal(10)) ] self.strategy.profit_taking_proposal(PositionMode.ONEWAY, positions) self.assertEqual(0, len(self.market.limit_orders)) self.assertTrue( self._is_logged( "INFO", f"Initiated cancellation of sell order {order_id} in favour of take profit order." ))
def test_account_positions(self): """ Test getting account positions by manually adding a position to the class member """ pt: PerpetualTrading = PerpetualTrading() aPos: Position = Position("market1", PositionSide.LONG, Decimal("0"), Decimal("100"), Decimal("1"), Decimal("5")) pt._account_positions["market1"] = aPos self.assertEqual(len(pt.account_positions), 1) self.assertEqual(pt.account_positions["market1"], aPos) self.assertEqual(pt.get_position("market1"), aPos) self.assertEqual(pt.get_position("market2"), None)
async def _update_positions(self): position_tasks = [] for pair in self._trading_pairs: position_tasks.append( self._api_request( "post", "perpfi/position", {"pair": convert_to_exchange_trading_pair(pair)})) positions = await safe_gather(*position_tasks, return_exceptions=True) for trading_pair, position in zip(self._trading_pairs, positions): position = position.get("position", {}) amount = self.quantize_order_amount(trading_pair, Decimal(position.get("size"))) if amount != Decimal("0"): position_side = PositionSide.LONG if amount > 0 else PositionSide.SHORT unrealized_pnl = self.quantize_order_amount( trading_pair, Decimal(position.get("pnl"))) entry_price = self.quantize_order_price( trading_pair, Decimal(position.get("entryPrice"))) leverage = self._leverage[trading_pair] self._account_positions[self.position_key( trading_pair)] = Position(trading_pair=trading_pair, position_side=position_side, unrealized_pnl=unrealized_pnl, entry_price=entry_price, amount=amount, leverage=leverage) else: if self.position_key(trading_pair) in self._account_positions: del self._account_positions[self.position_key( trading_pair)] payment = Decimal(str(position.get("fundingPayment"))) oldPayment = self._fundingPayment.get(trading_pair, 0) if payment != oldPayment: self._fundingPayment[trading_pair] = oldPayment action = "paid" if payment < 0 else "received" if payment != Decimal("0"): self.logger().info( f"Funding payment of {payment} {action} on {trading_pair} market." ) self.trigger_event( MarketEvent.FundingPaymentCompleted, FundingPaymentCompletedEvent( timestamp=time.time(), market=self.name, funding_rate=self._funding_info[trading_pair]. rate, trading_pair=trading_pair, amount=payment))
def test_stop_loss_order_recreated_after_wait_time_for_short_position( self): position = Position(trading_pair=(self.trading_pair), position_side=PositionSide.LONG, unrealized_pnl=Decimal(1000), entry_price=self.initial_mid_price - Decimal(30), amount=Decimal(-1), leverage=Decimal(10)) self.market.account_positions[self.trading_pair] = position initial_stop_loss_price = self.initial_mid_price + Decimal("0.1") initial_stop_loss_order_id = self.strategy.buy_with_specific_market( market_trading_pair_tuple=self.market_info, amount=Decimal(1), order_type=OrderType.LIMIT, price=initial_stop_loss_price) # Simulate first stop loss was created at timestamp 1000 self.strategy._exit_orders[ initial_stop_loss_order_id] = self.start_timestamp self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(1, len(self.strategy.active_orders)) self.assertEqual(initial_stop_loss_order_id, self.strategy.active_orders[0].client_order_id) self.clock.backtest_til(self.start_timestamp + 9) self.assertEqual(1, len(self.strategy.active_orders)) self.assertEqual(initial_stop_loss_order_id, self.strategy.active_orders[0].client_order_id) self.clock.backtest_til(self.start_timestamp + 10) self.assertEqual(initial_stop_loss_order_id, self.cancel_order_logger.event_log[0].order_id) self.assertEqual(1, len(self.strategy.active_orders)) new_stop_loss_order = self.strategy.active_orders[0] self.assertNotEqual(initial_stop_loss_order_id, new_stop_loss_order.client_order_id) self.assertTrue(new_stop_loss_order.is_buy) self.assertEqual(abs(position.amount), new_stop_loss_order.quantity) self.assertEqual( initial_stop_loss_price * (Decimal(1) + self.stop_loss_slippage_buffer), new_stop_loss_order.price)
def test_create_stop_loss_proposal_for_short_position(self): position = Position( trading_pair=(self.trading_pair), position_side=PositionSide.LONG, unrealized_pnl=Decimal(1000), entry_price=self.initial_mid_price - Decimal(30), amount=Decimal(-1), leverage=Decimal(10)) positions = [position] proposal = self.strategy.stop_loss_proposal(PositionMode.ONEWAY, positions) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(proposal.sells)) self.assertEqual(position.entry_price * (Decimal(1) + self.stop_loss_spread) * (Decimal(1) + self.stop_loss_slippage_buffer), proposal.buys[0].price) self.assertEqual(abs(position.amount), proposal.buys[0].size)
def test_strategy_starts_with_existing_position(self): """ Tests if the strategy can start """ self.clock.add_iterator(self.strategy) self.perp_connector._account_positions[trading_pair] = Position( trading_pair, PositionSide.SHORT, Decimal("0"), Decimal("95"), Decimal("-1"), self.perp_connector.get_leverage(trading_pair)) self.clock.backtest_til(self.start_timestamp + 1) self.assertTrue(self._is_logged("INFO", "Markets are ready.")) self.assertTrue(self._is_logged("INFO", "Trading started.")) self.assertTrue( self._is_logged( "INFO", f"There is an existing {trading_pair} " f"{PositionSide.SHORT.name} position. The bot resumes " f"operation to close out the arbitrage position")) asyncio.get_event_loop().run_until_complete(asyncio.sleep(0.01)) self.clock.backtest_til(self.start_timestamp + 2)
async def _update_positions(self): # local_position_names = set(self._account_positions.keys()) # remote_position_names = set() positions = await self.request(path="/fapi/v2/positionRisk", add_timestamp=True, is_signed=True) for position in positions: trading_pair = position.get("symbol") position_side = PositionSide[position.get("positionSide")] unrealized_pnl = Decimal(position.get("unRealizedProfit")) entry_price = Decimal(position.get("entryPrice")) amount = Decimal(position.get("positionAmt")) leverage = Decimal(position.get("leverage")) if amount != 0: self._account_positions[trading_pair + position_side.name] = Position( trading_pair=convert_from_exchange_trading_pair(trading_pair), position_side=position_side, unrealized_pnl=unrealized_pnl, entry_price=entry_price, amount=amount, leverage=leverage ) else: if (trading_pair + position_side.name) in self._account_positions: del self._account_positions[trading_pair + position_side.name]
def test_arbitrage_buy_spot_sell_perp(self): self.strategy._position_mode_ready = True self.clock.add_iterator(self.strategy) self.assertEqual(StrategyState.Closed, self.strategy.strategy_state) self.turn_clock(2) # self.clock.backtest_til(self.start_timestamp + 1) # asyncio.get_event_loop().run_until_complete(asyncio.sleep(0.01)) self.assertTrue( self._is_logged("INFO", "Arbitrage position opening opportunity found.")) self.assertTrue( self._is_logged( "INFO", "Profitability (8.96%) is now above min_opening_arbitrage_pct." )) self.assertTrue( self._is_logged( "INFO", "Placing BUY order for 1 HBOT at mock_paper_exchange at 100.500 price" )) self.assertTrue( self._is_logged( "INFO", "Placing SELL order for 1 HBOT at mock_perp_connector at 109.500 price " "to OPEN position.")) placed_orders = self.strategy.tracked_market_orders self.assertEqual(2, len(placed_orders)) spot_order = [ order for market, order in placed_orders if market == self.spot_connector ][0] self.assertTrue(spot_order.is_buy) self.assertEqual(Decimal("1"), Decimal(str(spot_order.amount))) perp_order = [ order for market, order in placed_orders if market == self.perp_connector ][0] self.assertFalse(perp_order.is_buy) self.assertEqual(Decimal("1"), Decimal(str(perp_order.amount))) self.assertEqual(StrategyState.Opening, self.strategy.strategy_state) self.trigger_order_complete(True, self.spot_connector, Decimal("1"), Decimal("100.5"), spot_order.order_id) self.trigger_order_complete(False, self.perp_connector, Decimal("1"), Decimal("109.5"), perp_order.order_id) self.perp_connector._account_positions[trading_pair] = Position( trading_pair, PositionSide.SHORT, Decimal("0"), Decimal("109.5"), Decimal("-1"), self.perp_connector.get_leverage(trading_pair)) self.turn_clock(1) status = asyncio.get_event_loop().run_until_complete( self.strategy.format_status()) expected_status = (""" Markets: Exchange Market Sell Price Buy Price Mid Price mock_paper_exchange HBOT-USDT 99.5 100.5 100 mock_perp_connector HBOT-USDT 109.5 110.5 110 Positions: Symbol Type Entry Price Amount Leverage Unrealized PnL HBOT-USDT SHORT 109.5 -1 5 0 Assets: Exchange Asset Total Balance Available Balance 0 mock_paper_exchange HBOT 5 5 1 mock_paper_exchange USDT 500 500 2 mock_perp_connector HBOT 5 5 3 mock_perp_connector USDT 500 500 Opportunity: buy at mock_paper_exchange, sell at mock_perp_connector: 8.96% sell at mock_paper_exchange, buy at mock_perp_connector: -9.95%""") self.assertEqual(expected_status, status) self.assertEqual(StrategyState.Opened, self.strategy.strategy_state) self.perp_connector.set_balanced_order_book(trading_pair=trading_pair, mid_price=90, min_price=1, max_price=200, price_step_size=1, volume_step_size=10) self.turn_clock(1) placed_orders = self.strategy.tracked_market_orders self.assertEqual(4, len(placed_orders)) spot_order = [ o for m, o in placed_orders if m == self.spot_connector and o.order_id != spot_order.order_id ][0] self.assertFalse(spot_order.is_buy) self.assertEqual(Decimal("1"), Decimal(str(spot_order.amount))) perp_order = [ o for m, o in placed_orders if m == self.perp_connector and o.order_id != perp_order.order_id ][0] self.assertTrue(perp_order.is_buy) self.assertEqual(Decimal("1"), Decimal(str(perp_order.amount))) self.assertEqual(StrategyState.Closing, self.strategy.strategy_state) self.trigger_order_complete(False, self.spot_connector, Decimal("1"), Decimal("99.5"), spot_order.order_id) self.trigger_order_complete(True, self.perp_connector, Decimal("1"), Decimal("90.5"), perp_order.order_id) self.perp_connector._account_positions.clear() self.turn_clock(1) # Due to the next_arbitrage_opening_delay, new arb position is not opened yet self.assertEqual(StrategyState.Closed, self.strategy.strategy_state) # Set balance on perpetual to 0 to test the strategy shouldn't submit orders self.spot_connector.set_balance(base_asset, 0) self.turn_clock(12) self.assertEqual(StrategyState.Closed, self.strategy.strategy_state) self.assertEqual(4, len(self.strategy.tracked_market_orders)) self.spot_connector.set_balance(base_asset, 10) self.turn_clock(1) # After next_arbitrage_opening_delay, new arb orders are submitted self.assertEqual(StrategyState.Opening, self.strategy.strategy_state) self.assertEqual(6, len(self.strategy.tracked_market_orders))