203 lines
6.9 KiB
Python
203 lines
6.9 KiB
Python
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,
|
|
)
|
|
|
|
MIN_NOTIONAL = 5.0 # 바이낸스 선물 최소 명목금액 (USDT)
|
|
|
|
def calculate_quantity(self, balance: float, price: float, leverage: int, margin_ratio: float) -> float:
|
|
"""동적 증거금 비율 기반 포지션 크기 계산 (최소 명목금액 $5 보장)"""
|
|
notional = balance * margin_ratio * leverage
|
|
if notional < self.MIN_NOTIONAL:
|
|
notional = self.MIN_NOTIONAL
|
|
quantity = notional / price
|
|
qty_rounded = round(quantity, 1)
|
|
if qty_rounded * price < self.MIN_NOTIONAL:
|
|
qty_rounded = round(self.MIN_NOTIONAL / price + 0.05, 1)
|
|
return qty_rounded
|
|
|
|
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
|
|
|
|
_ALGO_ORDER_TYPES = {"STOP_MARKET", "TAKE_PROFIT_MARKET", "STOP", "TAKE_PROFIT", "TRAILING_STOP_MARKET"}
|
|
|
|
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()
|
|
|
|
if order_type in self._ALGO_ORDER_TYPES:
|
|
return await self._place_algo_order(
|
|
side=side,
|
|
quantity=quantity,
|
|
order_type=order_type,
|
|
stop_price=stop_price,
|
|
reduce_only=reduce_only,
|
|
)
|
|
|
|
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 _place_algo_order(
|
|
self,
|
|
side: str,
|
|
quantity: float,
|
|
order_type: str,
|
|
stop_price: float = None,
|
|
reduce_only: bool = False,
|
|
) -> dict:
|
|
"""STOP_MARKET / TAKE_PROFIT_MARKET 등 Algo Order API(/fapi/v1/algoOrder)로 전송."""
|
|
loop = asyncio.get_event_loop()
|
|
params = dict(
|
|
symbol=self.config.symbol,
|
|
side=side,
|
|
algoType="CONDITIONAL",
|
|
type=order_type,
|
|
quantity=quantity,
|
|
reduceOnly="true" if reduce_only else "false",
|
|
)
|
|
if stop_price:
|
|
params["triggerPrice"] = stop_price
|
|
try:
|
|
return await loop.run_in_executor(
|
|
None, lambda: self.client.futures_create_algo_order(**params)
|
|
)
|
|
except BinanceAPIException as e:
|
|
logger.error(f"Algo 주문 실패: {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):
|
|
"""일반 오픈 주문과 Algo 오픈 주문을 모두 취소한다."""
|
|
loop = asyncio.get_event_loop()
|
|
await loop.run_in_executor(
|
|
None,
|
|
lambda: self.client.futures_cancel_all_open_orders(
|
|
symbol=self.config.symbol
|
|
),
|
|
)
|
|
try:
|
|
await loop.run_in_executor(
|
|
None,
|
|
lambda: self.client.futures_cancel_all_algo_open_orders(
|
|
symbol=self.config.symbol
|
|
),
|
|
)
|
|
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
|
|
|
|
async def create_listen_key(self) -> str:
|
|
"""POST /fapi/v1/listenKey — listenKey 신규 발급"""
|
|
loop = asyncio.get_event_loop()
|
|
result = await loop.run_in_executor(
|
|
None,
|
|
lambda: self.client.futures_stream_get_listen_key(),
|
|
)
|
|
return result
|
|
|
|
async def keepalive_listen_key(self, listen_key: str) -> None:
|
|
"""PUT /fapi/v1/listenKey — listenKey 만료 연장 (60분 → 리셋)"""
|
|
loop = asyncio.get_event_loop()
|
|
await loop.run_in_executor(
|
|
None,
|
|
lambda: self.client.futures_stream_keepalive(listenKey=listen_key),
|
|
)
|
|
|
|
async def delete_listen_key(self, listen_key: str) -> None:
|
|
"""DELETE /fapi/v1/listenKey — listenKey 삭제 (정상 종료 시)"""
|
|
loop = asyncio.get_event_loop()
|
|
try:
|
|
await loop.run_in_executor(
|
|
None,
|
|
lambda: self.client.futures_stream_close(listenKey=listen_key),
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"listenKey 삭제 실패 (무시): {e}")
|