62 lines
2.4 KiB
Python
62 lines
2.4 KiB
Python
import asyncio
|
|
import json
|
|
import subprocess
|
|
|
|
from pybreaker import CircuitBreaker
|
|
|
|
from hydra.exchange.base import BaseExchange
|
|
from hydra.resilience.circuit_breaker import create_breaker
|
|
from hydra.logging.setup import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
VALID_LEVERAGE = range(1, 126) # 1x ~ 125x
|
|
|
|
|
|
class CryptoExchange(BaseExchange):
|
|
"""ccxt CLI를 subprocess로 호출. 서킷 브레이커 적용."""
|
|
|
|
def __init__(self, exchange_id: str, breaker: CircuitBreaker | None = None, is_futures: bool = False):
|
|
self.exchange_id = exchange_id
|
|
self.is_futures = is_futures
|
|
self._breaker = breaker or create_breaker(f"crypto:{exchange_id}")
|
|
|
|
async def _run(self, args: list[str]) -> dict:
|
|
cmd = ["ccxt", self.exchange_id] + args + ["--json"]
|
|
loop = asyncio.get_event_loop()
|
|
raw = await loop.run_in_executor(
|
|
None,
|
|
lambda: self._breaker.call(subprocess.check_output, cmd, text=True, timeout=15),
|
|
)
|
|
return json.loads(raw)
|
|
|
|
async def get_balance(self) -> dict:
|
|
return await self._run(["fetchBalance"])
|
|
|
|
async def create_order(self, symbol: str, side: str, order_type: str, qty: float, price: float | None = None) -> dict:
|
|
args = ["createOrder", symbol, order_type, side, str(qty)]
|
|
if price:
|
|
args.append(str(price))
|
|
return await self._run(args)
|
|
|
|
async def cancel_order(self, order_id: str) -> dict:
|
|
return await self._run(["cancelOrder", order_id])
|
|
|
|
async def cancel_all(self) -> list:
|
|
result = await self._run(["cancelAllOrders"])
|
|
return result if isinstance(result, list) else []
|
|
|
|
async def get_positions(self) -> list:
|
|
result = await self._run(["fetchPositions"])
|
|
return result if isinstance(result, list) else []
|
|
|
|
async def set_leverage(self, symbol: str, leverage: int) -> None:
|
|
"""선물 레버리지 설정. 현물 모드에서는 no-op."""
|
|
if not self.is_futures:
|
|
logger.debug("set_leverage_skipped_spot", exchange=self.exchange_id, symbol=symbol)
|
|
return
|
|
if leverage not in VALID_LEVERAGE:
|
|
raise ValueError(f"레버리지는 1~125 사이여야 합니다. 입력값: {leverage}")
|
|
await self._run(["setLeverage", str(leverage), symbol])
|
|
logger.info("leverage_set", exchange=self.exchange_id, symbol=symbol, leverage=leverage)
|