Initial public release

This commit is contained in:
sinmb79
2026-03-30 13:19:11 +09:00
commit 92a692b63c
116 changed files with 5822 additions and 0 deletions
View File
+22
View File
@@ -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
+61
View File
@@ -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)
+41
View File
@@ -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
+63
View File
@@ -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
]
+41
View File
@@ -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 []