Initial public release
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BaseExchange(ABC):
|
||||
@abstractmethod
|
||||
async def get_balance(self) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
async def create_order(self, symbol: str, side: str, order_type: str, qty: float, price: float | None = None) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
async def cancel_order(self, order_id: str) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
async def cancel_all(self) -> list: ...
|
||||
|
||||
@abstractmethod
|
||||
async def get_positions(self) -> list: ...
|
||||
|
||||
async def set_leverage(self, symbol: str, leverage: int) -> None:
|
||||
"""레버리지 설정. 현물 거래소는 no-op (override 불필요)."""
|
||||
pass
|
||||
@@ -0,0 +1,61 @@
|
||||
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)
|
||||
@@ -0,0 +1,41 @@
|
||||
from hydra.config.markets import MarketManager
|
||||
from hydra.config.keys import KeyManager
|
||||
from hydra.exchange.base import BaseExchange
|
||||
from hydra.exchange.crypto import CryptoExchange
|
||||
from hydra.exchange.kis import KISExchange
|
||||
from hydra.exchange.polymarket import PolymarketExchange
|
||||
from hydra.logging.setup import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def create_exchanges(market_manager: MarketManager, key_manager: KeyManager) -> dict[str, BaseExchange]:
|
||||
"""활성화된 시장의 거래소 커넥터만 생성."""
|
||||
exchanges: dict[str, BaseExchange] = {}
|
||||
active = market_manager.get_active_markets()
|
||||
|
||||
for market in active:
|
||||
mode = market_manager.get_mode(market)
|
||||
is_paper = mode == "paper"
|
||||
try:
|
||||
if market == "kr":
|
||||
app_key, secret = key_manager.load("kis_kr")
|
||||
account_no, _ = key_manager.load("kis_account")
|
||||
exchanges["kr"] = KISExchange(app_key, secret, account_no, is_paper=is_paper)
|
||||
elif market == "us":
|
||||
app_key, secret = key_manager.load("kis_us")
|
||||
account_no, _ = key_manager.load("kis_account")
|
||||
exchanges["us"] = KISExchange(app_key, secret, account_no, is_paper=is_paper)
|
||||
elif market == "upbit":
|
||||
exchanges["upbit"] = CryptoExchange("upbit")
|
||||
elif market == "binance":
|
||||
exchanges["binance"] = CryptoExchange("binance")
|
||||
elif market == "hl":
|
||||
exchanges["hl"] = CryptoExchange("hyperliquid")
|
||||
elif market == "poly":
|
||||
exchanges["poly"] = PolymarketExchange()
|
||||
logger.info("exchange_created", market=market, mode=mode)
|
||||
except Exception as e:
|
||||
logger.error("exchange_create_failed", market=market, error=str(e))
|
||||
|
||||
return exchanges
|
||||
@@ -0,0 +1,63 @@
|
||||
from hydra.exchange.base import BaseExchange
|
||||
from hydra.logging.setup import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class KISExchange(BaseExchange):
|
||||
"""python-kis 래핑. 한국/미국 주식."""
|
||||
|
||||
def __init__(self, app_key: str, app_secret: str, account_no: str, is_paper: bool = True):
|
||||
self._app_key = app_key
|
||||
self._app_secret = app_secret
|
||||
self._account_no = account_no
|
||||
self._is_paper = is_paper
|
||||
self._client = None
|
||||
|
||||
def _get_client(self):
|
||||
if self._client is None:
|
||||
import pykis
|
||||
self._client = pykis.PyKis(
|
||||
id=self._app_key,
|
||||
account=self._account_no,
|
||||
appkey=self._app_key,
|
||||
appsecret=self._app_secret,
|
||||
virtual_account=self._is_paper,
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def get_balance(self) -> dict:
|
||||
client = self._get_client()
|
||||
account = client.account()
|
||||
return {"balance": float(account.balance)}
|
||||
|
||||
async def create_order(self, symbol: str, side: str, order_type: str, qty: float, price: float | None = None) -> dict:
|
||||
client = self._get_client()
|
||||
stock = client.stock(symbol)
|
||||
if side == "buy":
|
||||
order = stock.buy(qty=int(qty), price=price)
|
||||
else:
|
||||
order = stock.sell(qty=int(qty), price=price)
|
||||
return {"order_id": str(order.number), "status": "submitted"}
|
||||
|
||||
async def cancel_order(self, order_id: str) -> dict:
|
||||
client = self._get_client()
|
||||
client.cancel_order(order_id)
|
||||
return {"status": "canceled"}
|
||||
|
||||
async def cancel_all(self) -> list:
|
||||
client = self._get_client()
|
||||
orders = client.pending_orders()
|
||||
canceled = []
|
||||
for o in orders:
|
||||
o.cancel()
|
||||
canceled.append(str(o.number))
|
||||
return canceled
|
||||
|
||||
async def get_positions(self) -> list:
|
||||
client = self._get_client()
|
||||
account = client.account()
|
||||
return [
|
||||
{"symbol": s.symbol, "qty": s.qty, "avg_price": float(s.purchase_price), "side": "buy"}
|
||||
for s in account.stocks
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
from hydra.exchange.base import BaseExchange
|
||||
from hydra.logging.setup import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PolymarketExchange(BaseExchange):
|
||||
"""polymarket-cli subprocess 래핑."""
|
||||
|
||||
async def _run(self, args: list[str]) -> dict:
|
||||
cmd = ["polymarket"] + args + ["--json"]
|
||||
loop = asyncio.get_event_loop()
|
||||
raw = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: subprocess.check_output(cmd, text=True, timeout=15),
|
||||
)
|
||||
return json.loads(raw)
|
||||
|
||||
async def get_balance(self) -> dict:
|
||||
return await self._run(["balance"])
|
||||
|
||||
async def create_order(self, symbol: str, side: str, order_type: str, qty: float, price: float | None = None) -> dict:
|
||||
args = ["order", "create", "--market", symbol, "--side", side, "--amount", str(qty)]
|
||||
if price:
|
||||
args += ["--price", str(price)]
|
||||
return await self._run(args)
|
||||
|
||||
async def cancel_order(self, order_id: str) -> dict:
|
||||
return await self._run(["order", "cancel", "--id", order_id])
|
||||
|
||||
async def cancel_all(self) -> list:
|
||||
result = await self._run(["order", "cancel-all"])
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
async def get_positions(self) -> list:
|
||||
result = await self._run(["positions"])
|
||||
return result if isinstance(result, list) else []
|
||||
Reference in New Issue
Block a user