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
+16
View File
@@ -0,0 +1,16 @@
import typer
from hydra.cli import kill, status, trade, market, strategy, module
from hydra.cli import setup_wizard
app = typer.Typer(name="hydra", help="HYDRA 자동매매 시스템")
app.add_typer(kill.app, name="kill")
app.add_typer(status.app, name="status")
app.add_typer(trade.app, name="trade")
app.add_typer(market.app, name="market")
app.add_typer(strategy.app, name="strategy")
app.add_typer(module.app, name="module")
app.command("setup")(setup_wizard.run_setup)
if __name__ == "__main__":
app()
+30
View File
@@ -0,0 +1,30 @@
import asyncio
import typer
import httpx
from hydra.config.settings import get_settings
app = typer.Typer(help="Kill Switch - 전 포지션 즉시 청산")
@app.callback(invoke_without_command=True)
def kill(reason: str = typer.Option("cli_manual", help="청산 사유")):
"""전 포지션 즉시 시장가 청산."""
confirm = typer.confirm("[주의] 전 포지션을 청산합니다. 계속하시겠습니까?")
if not confirm:
typer.echo("취소됨.")
raise typer.Exit()
settings = get_settings()
try:
resp = httpx.post(
"http://127.0.0.1:8000/killswitch",
params={"reason": reason},
headers={"X-HYDRA-KEY": settings.hydra_api_key},
timeout=30,
)
resp.raise_for_status()
data = resp.json()
typer.echo(f"[완료] Kill Switch 실행. 청산: {len(data.get('closed', []))}")
except httpx.ConnectError:
typer.echo("[오류] HYDRA 서버에 연결할 수 없습니다. 서버가 실행 중인지 확인하세요.")
raise typer.Exit(1)
+31
View File
@@ -0,0 +1,31 @@
import typer
from hydra.config.markets import MarketManager
app = typer.Typer(help="시장 활성화/비활성화")
@app.command()
def enable(market: str, mode: str = typer.Option("paper", help="paper / live")):
"""시장 활성화."""
mm = MarketManager()
mm.enable(market, mode)
typer.echo(f"[완료] {market} 활성화 ({mode} 모드)")
@app.command()
def disable(market: str):
"""시장 비활성화."""
mm = MarketManager()
mm.disable(market)
typer.echo(f"[완료] {market} 비활성화")
@app.command()
def list_markets():
"""활성화된 시장 목록."""
mm = MarketManager()
active = mm.get_active_markets()
if active:
typer.echo("활성 시장: " + ", ".join(active))
else:
typer.echo("활성화된 시장 없음. 'hydra market enable <market>'로 활성화하세요.")
+24
View File
@@ -0,0 +1,24 @@
import typer
app = typer.Typer(help="AI 모듈 관리")
MODULES = ["regime_detection", "signal_scoring", "feature_selection", "adaptive_retrain",
"crash_detection", "dynamic_sizing", "sentiment"]
@app.command()
def enable(name: str):
if name not in MODULES:
typer.echo(f"[오류] 알 수 없는 모듈: {name}. 사용 가능: {MODULES}")
raise typer.Exit(1)
typer.echo(f"모듈 활성화: {name} - Phase 1에서 구현 예정")
@app.command()
def disable(name: str):
typer.echo(f"모듈 비활성화: {name} - Phase 1에서 구현 예정")
@app.command()
def list_modules():
typer.echo("AI 모듈 목록:\n" + "\n".join(f" - {m}" for m in MODULES))
+122
View File
@@ -0,0 +1,122 @@
import sys
import psutil
import typer
from pathlib import Path
from hydra.config.keys import KeyManager
from hydra.config.markets import MarketManager
from hydra.logging.setup import configure_logging
MARKETS = {
"kr": "한국 주식 (KIS)",
"us": "미국 주식 (KIS)",
"upbit": "업비트 (암호화폐)",
"binance": "바이낸스 (암호화폐)",
"hl": "Hyperliquid ([주의] 고위험)",
"poly": "Polymarket 예측시장 ([주의] 고위험)",
}
def detect_hardware() -> dict:
return {
"cpu_cores": psutil.cpu_count(logical=False),
"ram_gb": round(psutil.virtual_memory().total / 1024**3),
"disk_gb": round(psutil.disk_usage("/").total / 1024**3),
}
def recommend_profile(hw: dict) -> str:
if hw["ram_gb"] >= 32 and hw["cpu_cores"] >= 16:
return "expert"
elif hw["ram_gb"] >= 16 and hw["cpu_cores"] >= 8:
return "pro"
return "lite"
def run_setup():
"""7단계 HYDRA 초기 설정 위자드."""
configure_logging()
typer.echo("\nHYDRA 설정 위자드에 오신 것을 환영합니다.\n")
# Step 1: 하드웨어 감지
typer.echo("-- Step 1/7: 하드웨어 감지 --")
hw = detect_hardware()
typer.echo(f" CPU: {hw['cpu_cores']}코어 RAM: {hw['ram_gb']}GB Disk: {hw['disk_gb']}GB")
# Step 2: 프로필 추천
typer.echo("\n-- Step 2/7: 프로필 선택 --")
recommended = recommend_profile(hw)
typer.echo(f" 추천 프로필: {recommended.upper()}")
profile = typer.prompt(" 프로필 선택 [lite/pro/expert]", default=recommended)
if profile not in ("lite", "pro", "expert"):
typer.echo("[오류] 잘못된 프로필입니다. lite, pro, expert 중 선택하세요.")
raise typer.Exit(1)
# Step 3: AI 선택
typer.echo("\n-- Step 3/7: AI 모드 선택 --")
typer.echo(" [1] OFF (규칙 기반만) [2] 경량 CPU [3] GPU [4] 커스텀")
ai_choice = typer.prompt(" 선택", default="1")
ai_mode = {"1": "off", "2": "cpu", "3": "gpu", "4": "custom"}.get(ai_choice, "off")
# Step 4: 인터페이스
typer.echo("\n-- Step 4/7: 인터페이스 선택 --")
typer.echo(" [1] CLI+Telegram [2] Dashboard+Telegram [3] 전부 [4] Telegram만")
interface = typer.prompt(" 선택", default="1")
# Step 5: 면책조항 동의
typer.echo("\n-- Step 5/7: 면책조항 동의 --")
disclaimer = Path("DISCLAIMER.md").read_text(encoding="utf-8") if Path("DISCLAIMER.md").exists() else ""
typer.echo(disclaimer)
accepted = typer.confirm("\n위 면책조항에 동의하십니까?")
if not accepted:
typer.echo("면책조항에 동의하지 않으면 설치를 진행할 수 없습니다.")
sys.exit(1)
# Step 5b: 시장 선택 + API 키
typer.echo("\n-- 시장 선택 --")
selected_markets = []
mm = MarketManager()
km = KeyManager()
for market_id, label in MARKETS.items():
if typer.confirm(f" {label} 사용?", default=False):
selected_markets.append(market_id)
mode = typer.prompt(f" {label} 모드 [paper/live]", default="paper")
mm.enable(market_id, mode)
if market_id in ("kr", "us"):
app_key = typer.prompt(f" KIS App Key ({label})", hide_input=True)
secret = typer.prompt(f" KIS App Secret ({label})", hide_input=True)
km.store(f"kis_{market_id}", app_key, secret)
account_no = typer.prompt(" KIS 계좌번호 (예: 50123456-01)")
km.store("kis_account", account_no, "")
elif market_id in ("upbit", "binance", "hl"):
api_key = typer.prompt(f" {label} API Key", hide_input=True)
secret = typer.prompt(f" {label} Secret", hide_input=True)
km.store(market_id, api_key, secret)
# Step 6: 벤치마크
typer.echo("\n-- Step 6/7: 성능 벤치마크 실행 --")
typer.echo(" (잠시 기다려 주세요...)")
import subprocess
try:
subprocess.run(["python", "scripts/benchmark.py", "--profile", profile], timeout=30)
except Exception:
typer.echo(" 벤치마크 스킵 (scripts/benchmark.py 없음)")
# Step 7: 설정 저장
typer.echo("\n-- Step 7/7: 설정 완료 --")
env_content = f"""HYDRA_PROFILE={profile}
HYDRA_API_KEY={_generate_api_key()}
REDIS_URL=redis://localhost:6379
"""
Path(".env").write_text(env_content)
typer.echo("\n[완료] 설정이 저장되었습니다.")
typer.echo(f" 프로필: {profile.upper()} | AI: {ai_mode} | 시장: {', '.join(selected_markets) or '없음'}")
typer.echo(" hydra start 또는 docker compose -f docker-compose.lite.yml up 으로 시작하세요.")
def _generate_api_key() -> str:
import secrets
return secrets.token_urlsafe(32)
+60
View File
@@ -0,0 +1,60 @@
import typer
import httpx
from hydra.config.settings import get_settings
app = typer.Typer(help="시스템 상태 확인")
@app.callback(invoke_without_command=True)
def status():
"""HYDRA 상태 확인."""
settings = get_settings()
try:
h = httpx.get("http://127.0.0.1:8000/health", timeout=5)
s = httpx.get(
"http://127.0.0.1:8000/status",
headers={"X-HYDRA-KEY": settings.hydra_api_key},
timeout=5,
)
r = httpx.get(
"http://127.0.0.1:8000/risk",
headers={"X-HYDRA-KEY": settings.hydra_api_key},
timeout=5,
)
p = httpx.get(
"http://127.0.0.1:8000/pnl",
headers={"X-HYDRA-KEY": settings.hydra_api_key},
timeout=5,
)
typer.echo(f"[정상] 서버 상태 정상 | 프로필: {s.json()['profile']} | 가동시간: {h.json()['uptime_seconds']}")
risk = r.json()
ks = "ACTIVE" if risk["kill_switch_active"] else "NORMAL"
typer.echo(f"Kill Switch: {ks} | 일일 손익(리스크): {risk['daily_pnl_pct']*100:.2f}%")
pnl = p.json()
sign = lambda v: "+" if v >= 0 else ""
typer.echo(
f"\n=== 손익 현황 ===\n"
f" 실현 손익 (누적): {sign(pnl['realized_total'])}{pnl['realized_total']:,.4f} USDT\n"
f" 실현 손익 (오늘): {sign(pnl['daily_realized'])}{pnl['daily_realized']:,.4f} USDT\n"
f" 미실현 손익: {sign(pnl['unrealized'])}{pnl['unrealized']:,.4f} USDT\n"
f" 총 손익: {sign(pnl['total_pnl'])}{pnl['total_pnl']:,.4f} USDT\n"
f" 체결 거래 수: {pnl['trade_count']}"
)
if pnl["positions"]:
typer.echo("\n -- 오픈 포지션 --")
for pos in pnl["positions"]:
lev = f" {pos['leverage']}x" if pos.get("leverage", 1) > 1 else ""
upnl = pos.get("unrealized_pnl", 0)
typer.echo(
f" [{pos['market']}] {pos['symbol']} {pos['side'].upper()}{lev} "
f" 수량: {pos['qty']} 평균단가: {pos['avg_price']} "
f"미실현: {sign(upnl)}{upnl:,.4f}"
)
else:
typer.echo("\n 오픈 포지션 없음")
except httpx.ConnectError:
typer.echo("[오류] 서버 오프라인")
+18
View File
@@ -0,0 +1,18 @@
import typer
app = typer.Typer(help="전략 관리 (Phase 2에서 구현)")
@app.command()
def list_strategies():
typer.echo("전략 목록 - Phase 2에서 구현 예정")
@app.command()
def start(name: str):
typer.echo(f"전략 시작: {name} - Phase 2에서 구현 예정")
@app.command()
def stop(name: str):
typer.echo(f"전략 중지: {name} - Phase 2에서 구현 예정")
+40
View File
@@ -0,0 +1,40 @@
import typer
from typing import Optional
app = typer.Typer(help="수동 매매 명령 (Phase 1에서 전략 연동)")
@app.command()
def kr(symbol: str, side: str, qty: float):
"""한국 주식 수동 주문."""
typer.echo(f"[한국주식] {side} {symbol} {qty}주 - Phase 1에서 구현 예정")
@app.command()
def us(symbol: str, side: str, qty: float):
"""미국 주식 수동 주문."""
typer.echo(f"[미국주식] {side} {symbol} {qty}주 - Phase 1에서 구현 예정")
@app.command()
def crypto(
exchange: str,
symbol: str,
side: str,
qty: float,
leverage: int = typer.Option(1, "--leverage", "-l", help="선물 레버리지 배수 (1~125x). 현물은 1로 고정.", min=1, max=125),
futures: bool = typer.Option(False, "--futures", help="선물 주문 여부"),
):
"""암호화폐 수동 주문. 선물은 --futures --leverage N 으로 레버리지 지정."""
if futures and leverage > 1:
typer.echo(f"[{exchange}] {side} {symbol} {qty} x {leverage} 레버리지 (선물) - Phase 1에서 구현 예정")
elif futures:
typer.echo(f"[{exchange}] {side} {symbol} {qty} (선물) - Phase 1에서 구현 예정")
else:
typer.echo(f"[{exchange}] {side} {symbol} {qty} (현물) - Phase 1에서 구현 예정")
@app.command()
def poly(market_id: str, side: str, amount: float):
"""Polymarket 예측시장 주문."""
typer.echo(f"[Polymarket] {side} {market_id} ${amount} - Phase 1에서 구현 예정")