feat: BinanceFuturesClient 구현

Made-with: Cursor
This commit is contained in:
21in7
2026-03-01 12:49:26 +09:00
parent 81b7a7f714
commit b1a7632bbe
2 changed files with 129 additions and 0 deletions

92
src/exchange.py Normal file
View File

@@ -0,0 +1,92 @@
import asyncio
from binance.client import Client
from binance.exceptions import BinanceAPIException
from loguru import logger
from src.config import Config
class BinanceFuturesClient:
def __init__(self, config: Config):
self.config = config
self.client = Client(
api_key=config.api_key,
api_secret=config.api_secret,
)
def calculate_quantity(self, balance: float, price: float, leverage: int) -> float:
"""리스크 기반 포지션 크기 계산"""
risk_amount = balance * self.config.risk_per_trade
notional = risk_amount * leverage
quantity = notional / price
return round(quantity, 1)
async def set_leverage(self, leverage: int) -> dict:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: self.client.futures_change_leverage(
symbol=self.config.symbol, leverage=leverage
),
)
async def get_balance(self) -> float:
loop = asyncio.get_event_loop()
balances = await loop.run_in_executor(
None, self.client.futures_account_balance
)
for b in balances:
if b["asset"] == "USDT":
return float(b["balance"])
return 0.0
async def place_order(
self,
side: str,
quantity: float,
order_type: str = "MARKET",
price: float = None,
stop_price: float = None,
reduce_only: bool = False,
) -> dict:
loop = asyncio.get_event_loop()
params = dict(
symbol=self.config.symbol,
side=side,
type=order_type,
quantity=quantity,
reduceOnly=reduce_only,
)
if price:
params["price"] = price
params["timeInForce"] = "GTC"
if stop_price:
params["stopPrice"] = stop_price
try:
return await loop.run_in_executor(
None, lambda: self.client.futures_create_order(**params)
)
except BinanceAPIException as e:
logger.error(f"주문 실패: {e}")
raise
async def get_position(self) -> dict | None:
loop = asyncio.get_event_loop()
positions = await loop.run_in_executor(
None,
lambda: self.client.futures_position_information(
symbol=self.config.symbol
),
)
for p in positions:
if float(p["positionAmt"]) != 0:
return p
return None
async def cancel_all_orders(self):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: self.client.futures_cancel_all_open_orders(
symbol=self.config.symbol
),
)

37
tests/test_exchange.py Normal file
View File

@@ -0,0 +1,37 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from src.exchange import BinanceFuturesClient
from src.config import Config
import os
@pytest.fixture
def config():
os.environ.update({
"BINANCE_API_KEY": "test_key",
"BINANCE_API_SECRET": "test_secret",
"SYMBOL": "XRPUSDT",
"LEVERAGE": "10",
"RISK_PER_TRADE": "0.02",
})
return Config()
@pytest.mark.asyncio
async def test_set_leverage(config):
client = BinanceFuturesClient(config)
with patch.object(
client.client,
"futures_change_leverage",
return_value={"leverage": 10},
):
result = await client.set_leverage(10)
assert result is not None
def test_calculate_quantity(config):
client = BinanceFuturesClient(config)
# 잔고 1000 USDT, 리스크 2%, 레버리지 10, 가격 0.5
qty = client.calculate_quantity(balance=1000.0, price=0.5, leverage=10)
# 1000 * 0.02 * 10 / 0.5 = 400
assert qty == pytest.approx(400.0, rel=0.01)