From 2b315ad6d72c3834554bbe9a145ae33c4c58b1a1 Mon Sep 17 00:00:00 2001 From: 21in7 Date: Mon, 2 Mar 2026 13:46:25 +0900 Subject: [PATCH] feat: add get_open_interest and get_funding_rate to BinanceFuturesClient Made-with: Cursor --- src/exchange.py | 26 +++++++++++++++++++ tests/test_exchange.py | 59 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/exchange.py b/src/exchange.py index 37fe003..046b2fd 100644 --- a/src/exchange.py +++ b/src/exchange.py @@ -146,3 +146,29 @@ class BinanceFuturesClient: ) except Exception as e: logger.warning(f"Algo 주문 전체 취소 실패 (무시): {e}") + + async def get_open_interest(self) -> float | None: + """현재 미결제약정(OI)을 조회한다. 오류 시 None 반환.""" + loop = asyncio.get_event_loop() + try: + result = await loop.run_in_executor( + None, + lambda: self.client.futures_open_interest(symbol=self.config.symbol), + ) + return float(result["openInterest"]) + except Exception as e: + logger.warning(f"OI 조회 실패 (무시): {e}") + return None + + async def get_funding_rate(self) -> float | None: + """현재 펀딩비를 조회한다. 오류 시 None 반환.""" + loop = asyncio.get_event_loop() + try: + result = await loop.run_in_executor( + None, + lambda: self.client.futures_mark_price(symbol=self.config.symbol), + ) + return float(result["lastFundingRate"]) + except Exception as e: + logger.warning(f"펀딩비 조회 실패 (무시): {e}") + return None diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 64b6345..92e1505 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -25,6 +25,21 @@ def client(): return c +@pytest.fixture +def exchange(): + os.environ.update({ + "BINANCE_API_KEY": "test_key", + "BINANCE_API_SECRET": "test_secret", + "SYMBOL": "XRPUSDT", + "LEVERAGE": "10", + }) + config = Config() + c = BinanceFuturesClient.__new__(BinanceFuturesClient) + c.config = config + c.client = MagicMock() + return c + + @pytest.mark.asyncio async def test_set_leverage(config): with patch("src.exchange.Client") as MockClient: @@ -54,3 +69,47 @@ def test_calculate_quantity_zero_balance(client): """잔고 0이면 최소 명목금액 기반 수량 반환""" qty = client.calculate_quantity(balance=0.0, price=2.5, leverage=10, margin_ratio=0.50) assert qty > 0 + + +@pytest.mark.asyncio +async def test_get_open_interest(exchange): + """get_open_interest()가 float을 반환하는지 확인.""" + exchange.client.futures_open_interest = MagicMock( + return_value={"openInterest": "123456.789"} + ) + result = await exchange.get_open_interest() + assert isinstance(result, float) + assert result == pytest.approx(123456.789) + + +@pytest.mark.asyncio +async def test_get_funding_rate(exchange): + """get_funding_rate()가 float을 반환하는지 확인.""" + exchange.client.futures_mark_price = MagicMock( + return_value={"lastFundingRate": "0.0001"} + ) + result = await exchange.get_funding_rate() + assert isinstance(result, float) + assert result == pytest.approx(0.0001) + + +@pytest.mark.asyncio +async def test_get_open_interest_error_returns_none(exchange): + """API 오류 시 None 반환 확인.""" + from binance.exceptions import BinanceAPIException + exchange.client.futures_open_interest = MagicMock( + side_effect=BinanceAPIException(MagicMock(status_code=400), 400, '{"code":-1121,"msg":"Invalid symbol"}') + ) + result = await exchange.get_open_interest() + assert result is None + + +@pytest.mark.asyncio +async def test_get_funding_rate_error_returns_none(exchange): + """API 오류 시 None 반환 확인.""" + from binance.exceptions import BinanceAPIException + exchange.client.futures_mark_price = MagicMock( + side_effect=BinanceAPIException(MagicMock(status_code=400), 400, '{"code":-1121,"msg":"Invalid symbol"}') + ) + result = await exchange.get_funding_rate() + assert result is None