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
+11
View File
@@ -0,0 +1,11 @@
from fastapi import HTTPException, Header
from hydra.config.settings import get_settings
API_KEY_HEADER = "X-HYDRA-KEY"
async def verify_api_key(x_hydra_key: str = Header(..., alias=API_KEY_HEADER)) -> str:
settings = get_settings()
if x_hydra_key != settings.hydra_api_key:
raise HTTPException(status_code=403, detail="Invalid API key")
return x_hydra_key
+61
View File
@@ -0,0 +1,61 @@
# hydra/api/backtest.py
from dataclasses import asdict
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from hydra.api.auth import verify_api_key
from hydra.backtest.runner import BacktestRunner
from hydra.indicator.calculator import IndicatorCalculator
from hydra.regime.detector import RegimeDetector
from hydra.strategy.signal import SignalGenerator
router = APIRouter(prefix="/backtest")
_store = None
def set_store_for_backtest(store) -> None:
global _store
_store = store
class BacktestRequest(BaseModel):
market: str
symbol: str
timeframe: str
since: int
until: int
initial_capital: float = 10000.0
trade_amount_usd: float = 100.0
commission_pct: float = 0.001
@router.post("/run")
async def run_backtest(
req: BacktestRequest,
_: str = Depends(verify_api_key),
):
if _store is None:
raise HTTPException(status_code=503, detail="Store not initialized")
if req.since >= req.until:
raise HTTPException(status_code=400, detail="since must be less than until")
runner = BacktestRunner(
store=_store,
calculator=IndicatorCalculator(),
detector=RegimeDetector(),
generator=SignalGenerator(),
initial_capital=req.initial_capital,
trade_amount_usd=req.trade_amount_usd,
commission_pct=req.commission_pct,
)
try:
result = await runner.run(
market=req.market,
symbol=req.symbol,
timeframe=req.timeframe,
since=req.since,
until=req.until,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return asdict(result)
+50
View File
@@ -0,0 +1,50 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from hydra.api.auth import verify_api_key
from hydra.data.storage.base import OhlcvStore
router = APIRouter(prefix="/data")
_store: Optional[OhlcvStore] = None
def set_store(store: OhlcvStore) -> None:
global _store
_store = store
@router.get("/candles")
async def get_candles(
market: str,
symbol: str,
timeframe: str,
limit: int = Query(default=200, ge=1, le=1000),
since: Optional[int] = None,
_: str = Depends(verify_api_key),
):
"""Return OHLCV candles ordered by open_time ASC."""
if _store is None:
raise HTTPException(status_code=503, detail="Store not initialized")
candles = await _store.query(market, symbol, timeframe, limit=limit, since=since)
return [
{
"market": c.market,
"symbol": c.symbol,
"timeframe": c.timeframe,
"open_time": c.open_time,
"open": c.open,
"high": c.high,
"low": c.low,
"close": c.close,
"volume": c.volume,
"close_time": c.close_time,
}
for c in candles
]
@router.get("/symbols")
async def get_symbols(_: str = Depends(verify_api_key)):
"""Return distinct {market, symbol, timeframe} records being collected."""
if _store is None:
raise HTTPException(status_code=503, detail="Store not initialized")
return await _store.get_symbols()
+30
View File
@@ -0,0 +1,30 @@
import time
from fastapi import APIRouter
router = APIRouter()
_START_TIME = time.time()
_redis = None
def set_redis(redis_client) -> None:
global _redis
_redis = redis_client
@router.get("/health")
async def health():
result = {
"status": "ok",
"uptime_seconds": int(time.time() - _START_TIME),
}
if _redis:
keys = _redis.keys("hydra:collector:*:status")
collectors = {}
for key in keys:
market = key.split(":")[2]
collectors[market] = _redis.get(key)
if collectors:
result["collectors"] = collectors
if any(v and v.startswith("error:") for v in collectors.values()):
result["status"] = "degraded"
return result
+56
View File
@@ -0,0 +1,56 @@
import json
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from hydra.api.auth import verify_api_key
router = APIRouter(prefix="/data")
_redis = None
def set_redis_for_indicators(redis_client) -> None:
global _redis
_redis = redis_client
@router.get("/indicators")
async def get_indicators(
market: str,
symbol: str,
timeframe: str,
_: str = Depends(verify_api_key),
):
"""Return the latest cached indicator values for a symbol."""
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
key = f"hydra:indicator:{market}:{symbol}:{timeframe}"
raw = _redis.get(key)
if raw is None:
raise HTTPException(status_code=404, detail="No indicators cached for this symbol")
return {
"market": market,
"symbol": symbol,
"timeframe": timeframe,
"indicators": json.loads(raw),
}
@router.get("/indicators/list")
async def list_indicators(_: str = Depends(verify_api_key)):
"""Return all (market, symbol, timeframe) tuples that have cached indicators."""
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
keys = _redis.keys("hydra:indicator:*")
result = []
for key in keys:
parts = key.split(":")
# key format: hydra:indicator:{market}:{symbol}:{timeframe}
# symbol may contain ":" (e.g. BTC/USDT:USDT for futures)
# parts[0]="hydra", parts[1]="indicator", parts[2]=market,
# parts[3:-1]=symbol (joined), parts[-1]=timeframe
if len(parts) >= 5:
result.append({
"market": parts[2],
"symbol": ":".join(parts[3:-1]),
"timeframe": parts[-1],
})
return result
+27
View File
@@ -0,0 +1,27 @@
from fastapi import APIRouter, Depends
from hydra.api.auth import verify_api_key
router = APIRouter()
_market_manager = None
def set_market_manager(mm) -> None:
global _market_manager
_market_manager = mm
@router.get("/markets")
async def get_markets(_: str = Depends(verify_api_key)):
return {"active": _market_manager.get_active_markets()}
@router.post("/markets/{market_id}/enable")
async def enable_market(market_id: str, _: str = Depends(verify_api_key)):
_market_manager.enable(market_id)
return {"status": "enabled", "market": market_id}
@router.post("/markets/{market_id}/disable")
async def disable_market(market_id: str, _: str = Depends(verify_api_key)):
_market_manager.disable(market_id)
return {"status": "disabled", "market": market_id}
+16
View File
@@ -0,0 +1,16 @@
from fastapi import APIRouter, Depends
from hydra.api.auth import verify_api_key
from hydra.core.order_queue import OrderRequest
router = APIRouter()
_order_queue = None
def set_order_queue(queue) -> None:
global _order_queue
_order_queue = queue
@router.post("/orders")
async def create_order(order: OrderRequest, _: str = Depends(verify_api_key)):
return await _order_queue.submit(order)
+26
View File
@@ -0,0 +1,26 @@
from fastapi import APIRouter, Depends
from hydra.api.auth import verify_api_key
router = APIRouter()
_pnl_tracker = None
_position_tracker = None
def set_pnl_dependencies(pnl_tracker, position_tracker) -> None:
global _pnl_tracker, _position_tracker
_pnl_tracker = pnl_tracker
_position_tracker = position_tracker
@router.get("/pnl")
async def get_pnl(_: str = Depends(verify_api_key)):
"""전체 시스템 손익 현황."""
positions = _position_tracker.get_all()
return _pnl_tracker.get_summary(positions)
@router.post("/pnl/reset-daily")
async def reset_daily_pnl(_: str = Depends(verify_api_key)):
"""일일 손익 초기화."""
_pnl_tracker.reset_daily()
return {"status": "reset"}
+15
View File
@@ -0,0 +1,15 @@
from fastapi import APIRouter, Depends
from hydra.api.auth import verify_api_key
router = APIRouter()
_position_tracker = None
def set_position_tracker(tracker) -> None:
global _position_tracker
_position_tracker = tracker
@router.get("/positions")
async def get_positions(_: str = Depends(verify_api_key)):
return {"positions": _position_tracker.get_all()}
+52
View File
@@ -0,0 +1,52 @@
# hydra/api/regime.py
import json
from fastapi import APIRouter, Depends, HTTPException
from hydra.api.auth import verify_api_key
router = APIRouter(prefix="/data")
_redis = None
def set_redis_for_regime(redis_client) -> None:
global _redis
_redis = redis_client
@router.get("/regime")
async def get_regime(
market: str,
symbol: str,
timeframe: str,
_: str = Depends(verify_api_key),
):
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
key = f"hydra:regime:{market}:{symbol}:{timeframe}"
raw = _redis.get(key)
if raw is None:
raise HTTPException(status_code=404, detail="No regime cached for this symbol")
data = json.loads(raw)
return {
"market": market,
"symbol": symbol,
"timeframe": timeframe,
"regime": data["regime"],
"detected_at": data["detected_at"],
}
@router.get("/regime/list")
async def list_regimes(_: str = Depends(verify_api_key)):
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
keys = _redis.keys("hydra:regime:*")
result = []
for key in keys:
parts = key.split(":")
if len(parts) >= 5:
result.append({
"market": parts[2],
"symbol": ":".join(parts[3:-1]),
"timeframe": parts[-1],
})
return result
+26
View File
@@ -0,0 +1,26 @@
from fastapi import APIRouter, Depends
from hydra.api.auth import verify_api_key
router = APIRouter()
_kill_switch = None
_risk_engine = None
def set_dependencies(kill_switch, risk_engine) -> None:
global _kill_switch, _risk_engine
_kill_switch = kill_switch
_risk_engine = risk_engine
@router.get("/risk")
async def get_risk(_: str = Depends(verify_api_key)):
return {
"daily_pnl_pct": _risk_engine.get_daily_pnl_pct(),
"kill_switch_active": _kill_switch.is_active(),
}
@router.post("/killswitch")
async def killswitch(reason: str = "manual", _: str = Depends(verify_api_key)):
result = await _kill_switch.execute(reason=reason, source="api")
return {"success": result.success, "closed": result.closed_positions, "errors": result.errors}
+54
View File
@@ -0,0 +1,54 @@
# hydra/api/signals.py
import json
from fastapi import APIRouter, Depends, HTTPException
from hydra.api.auth import verify_api_key
router = APIRouter(prefix="/data")
_redis = None
def set_redis_for_signals(redis_client) -> None:
global _redis
_redis = redis_client
@router.get("/signal")
async def get_signal(
market: str,
symbol: str,
timeframe: str,
_: str = Depends(verify_api_key),
):
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
key = f"hydra:signal:{market}:{symbol}:{timeframe}"
raw = _redis.get(key)
if raw is None:
raise HTTPException(status_code=404, detail="No signal cached for this symbol")
data = json.loads(raw)
return {
"market": market,
"symbol": symbol,
"timeframe": timeframe,
"signal": data["signal"],
"reason": data["reason"],
"price": data["price"],
"ts": data["ts"],
}
@router.get("/signal/list")
async def list_signals(_: str = Depends(verify_api_key)):
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
keys = _redis.keys("hydra:signal:*")
result = []
for key in keys:
parts = key.split(":")
if len(parts) >= 5:
result.append({
"market": parts[2],
"symbol": ":".join(parts[3:-1]),
"timeframe": parts[-1],
})
return result
+14
View File
@@ -0,0 +1,14 @@
from fastapi import APIRouter, Depends
from hydra.api.auth import verify_api_key
router = APIRouter()
@router.get("/strategies")
async def list_strategies(_: str = Depends(verify_api_key)):
return {"strategies": [], "note": "Phase 1에서 구현 예정"}
@router.post("/strategies/{name}/start")
async def start_strategy(name: str, _: str = Depends(verify_api_key)):
return {"status": "not_implemented", "note": "Phase 1에서 구현 예정"}
+53
View File
@@ -0,0 +1,53 @@
# hydra/api/supplemental.py
import json
from fastapi import APIRouter, Depends, HTTPException
from hydra.api.auth import verify_api_key
router = APIRouter(prefix="/data")
_redis = None
def set_redis_for_supplemental(redis_client) -> None:
global _redis
_redis = redis_client
@router.get("/orderbook")
async def get_orderbook(
market: str,
symbol: str,
_: str = Depends(verify_api_key),
):
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
key = f"hydra:orderbook:{market}:{symbol}"
raw = _redis.get(key)
if raw is None:
raise HTTPException(status_code=404, detail="No orderbook cached for this symbol")
data = json.loads(raw)
return {"market": market, "symbol": symbol, **data}
@router.get("/events")
async def get_events(_: str = Depends(verify_api_key)):
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
raw = _redis.get("hydra:events:upcoming")
if raw is None:
return []
return json.loads(raw)
@router.get("/sentiment")
async def get_sentiment(
symbol: str,
_: str = Depends(verify_api_key),
):
if _redis is None:
raise HTTPException(status_code=503, detail="Redis not initialized")
key = f"hydra:sentiment:{symbol}"
raw = _redis.get(key)
if raw is None:
raise HTTPException(status_code=404, detail="No sentiment cached for this symbol")
data = json.loads(raw)
return {"symbol": symbol, **data}
+20
View File
@@ -0,0 +1,20 @@
import hydra
from fastapi import APIRouter, Depends
from hydra.api.auth import verify_api_key
from hydra.config.settings import get_settings
router = APIRouter()
@router.get("/status")
async def status(_: str = Depends(verify_api_key)):
settings = get_settings()
return {
"version": hydra.__version__,
"profile": settings.hydra_profile,
}
@router.get("/modules")
async def modules(_: str = Depends(verify_api_key)):
return {"modules": ["core", "resilience", "exchange", "notify"]}