""" 전략 파라미터 관리 API GET /api/strategy-config — 전체 파라미터 목록 GET /api/strategy-config/groups — 그룹 목록 GET /api/strategy-config/{key} — 단일 파라미터 조회 PUT /api/strategy-config/{key} — 파라미터 값 수정 POST /api/strategy-config/{key}/reset — 단일 파라미터 기본값 리셋 POST /api/strategy-config/reset-all — 전체 기본값 리셋 PATCH /api/strategy-config/{key}/toggle — is_active 토글 """ import logging from typing import Any, Dict, List, Optional from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from sqlalchemy import select, update from app.core.database import AsyncSessionLocal from app.models.models import StrategyConfig from app.services.strategy_config_service import ( get_effective_dt_params_v2, serialize_config_v2, strategy_cfg, ) router = APIRouter() logger = logging.getLogger(__name__) # ── Pydantic 스키마 ─────────────────────────────────────────────────────────── class StrategyConfigResponse(BaseModel): id: int key: str group: str label: str description: Optional[str] value_type: str value: str default_value: str min_value: Optional[str] max_value: Optional[str] is_active: bool updated_by: Optional[str] read_only: bool = False class Config: from_attributes = True class UpdateValueRequest(BaseModel): value: str updated_by: Optional[str] = "api" class BulkUpdateItem(BaseModel): key: str value: str class BulkUpdateRequest(BaseModel): items: List[BulkUpdateItem] updated_by: Optional[str] = "api" # ── 엔드포인트 ──────────────────────────────────────────────────────────────── @router.get("", response_model=List[StrategyConfigResponse]) async def list_configs( group: Optional[str] = Query(None, description="그룹 필터 (INDICATOR/RISK/FILTER/DAY_TRADE/MEAN_REVERSION/GENERAL)"), active_only: bool = Query(False, description="활성 파라미터만 조회") ): """전체 전략 파라미터 목록 조회.""" async with AsyncSessionLocal() as session: stmt = select(StrategyConfig).order_by(StrategyConfig.group, StrategyConfig.key) if group: stmt = stmt.where(StrategyConfig.group == group.upper()) if active_only: stmt = stmt.where(StrategyConfig.is_active == True) result = await session.execute(stmt) return [serialize_config_v2(c) for c in result.scalars().all()] @router.get("/groups", response_model=List[str]) async def list_groups(): """사용 중인 그룹 목록 반환.""" return ["INDICATOR", "RISK", "FILTER", "DAY_TRADE", "MEAN_REVERSION", "GENERAL"] @router.get("/dt-effective", response_model=Dict[str, Any]) async def get_dt_effective(): """DT 엔진이 실제로 쓰는 값(settings 단일 소스). 읽기 전용.""" return get_effective_dt_params_v2() @router.get("/cache-snapshot", response_model=Dict[str, Any]) async def get_cache_snapshot(): """현재 인메모리 캐시 상태 조회 (디버그용).""" return strategy_cfg._cache @router.get("/{key}", response_model=StrategyConfigResponse) async def get_config(key: str): """단일 파라미터 조회.""" async with AsyncSessionLocal() as session: result = await session.execute( select(StrategyConfig).where(StrategyConfig.key == key) ) cfg = result.scalar_one_or_none() if not cfg: raise HTTPException(status_code=404, detail=f"파라미터 '{key}'를 찾을 수 없습니다.") return serialize_config_v2(cfg) @router.put("/{key}", response_model=StrategyConfigResponse) async def update_config(key: str, body: UpdateValueRequest): """파라미터 값 수정. 수정 즉시 캐시 무효화 → 30초 이내 전략 코드 반영.""" try: cfg = await strategy_cfg.upsert(key, body.value, body.updated_by or "api") except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) if not cfg: raise HTTPException(status_code=404, detail=f"파라미터 '{key}'를 찾을 수 없습니다.") return cfg @router.put("", response_model=Dict[str, Any]) async def bulk_update(body: BulkUpdateRequest): """여러 파라미터 일괄 수정.""" results = {"updated": [], "failed": []} for item in body.items: try: cfg = await strategy_cfg.upsert(item.key, item.value, body.updated_by or "api") if cfg: results["updated"].append(item.key) else: results["failed"].append({"key": item.key, "reason": "not_found"}) except ValueError as e: results["failed"].append({"key": item.key, "reason": str(e)}) return results @router.post("/{key}/reset", response_model=StrategyConfigResponse) async def reset_config(key: str): """단일 파라미터를 기본값으로 리셋.""" try: cfg = await strategy_cfg.reset_to_default(key) except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) if not cfg: raise HTTPException(status_code=404, detail=f"파라미터 '{key}'를 찾을 수 없습니다.") return cfg @router.post("/reset-all", response_model=Dict[str, str]) async def reset_all_configs(): """모든 파라미터를 기본값으로 리셋.""" await strategy_cfg.reset_all_to_default() return {"status": "ok", "message": "전체 파라미터가 기본값으로 초기화되었습니다."} @router.patch("/{key}/toggle", response_model=StrategyConfigResponse) async def toggle_active(key: str): """is_active 토글 (비활성화 시 해당 파라미터는 config.py 기본값 사용).""" async with AsyncSessionLocal() as session: result = await session.execute( select(StrategyConfig).where(StrategyConfig.key == key) ) cfg = result.scalar_one_or_none() if not cfg: raise HTTPException(status_code=404, detail=f"파라미터 '{key}'를 찾을 수 없습니다.") cfg.is_active = not cfg.is_active await session.commit() await session.refresh(cfg) await strategy_cfg.invalidate() return cfg