feat: add get_open_interest and get_funding_rate to BinanceFuturesClient

Made-with: Cursor
This commit is contained in:
21in7
2026-03-02 13:46:25 +09:00
parent 7a1abc7b72
commit 2b315ad6d7
2 changed files with 85 additions and 0 deletions

View File

@@ -146,3 +146,29 @@ class BinanceFuturesClient:
) )
except Exception as e: except Exception as e:
logger.warning(f"Algo 주문 전체 취소 실패 (무시): {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

View File

@@ -25,6 +25,21 @@ def client():
return c 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 @pytest.mark.asyncio
async def test_set_leverage(config): async def test_set_leverage(config):
with patch("src.exchange.Client") as MockClient: with patch("src.exchange.Client") as MockClient:
@@ -54,3 +69,47 @@ def test_calculate_quantity_zero_balance(client):
"""잔고 0이면 최소 명목금액 기반 수량 반환""" """잔고 0이면 최소 명목금액 기반 수량 반환"""
qty = client.calculate_quantity(balance=0.0, price=2.5, leverage=10, margin_ratio=0.50) qty = client.calculate_quantity(balance=0.0, price=2.5, leverage=10, margin_ratio=0.50)
assert qty > 0 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