Initial public release
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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)
|
||||
@@ -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"}
|
||||
@@ -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()}
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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에서 구현 예정"}
|
||||
@@ -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}
|
||||
@@ -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"]}
|
||||
Reference in New Issue
Block a user