feat: BinanceFuturesClient 구현
Made-with: Cursor
This commit is contained in:
92
src/exchange.py
Normal file
92
src/exchange.py
Normal 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
37
tests/test_exchange.py
Normal 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)
|
||||||
Reference in New Issue
Block a user