feat: 전략 파라미터 DB 관리 시스템 구현
[추가 파일]
1. models/models.py — StrategyConfig 테이블 추가
2. services/strategy_config_service.py — DB 읽기 + TTL 캐시 서비스
3. api/endpoints/strategy_config.py — CRUD REST API
4. api/__init__.py — strategy_config 라우터 등록
[동작 방식]
- DB에 파라미터 있으면 DB 우선, 없으면 config.py 기본값 fallback
- TTL 30초 인메모리 캐시 → 재시작 없이 실시간 파라미터 변경
- 전략 코드에서 strategy_cfg.get("KEY", default) 패턴으로 사용
- /api/strategy-config 엔드포인트로 UI에서 관리 가능
[관리 대상 파라미터 그룹]
- INDICATOR: RSI, 볼린저, MA, ATR, ADX
- RISK: 손절/익절, MDD, 포지션 크기, 연속손실
- FILTER: 과열필터, 하락장필터, 트렌드필터
- DAY_TRADE: 데이트레이딩 세부 파라미터
- MEAN_REVERSION: 평균회귀 전략 파라미터
This commit is contained in:
165
backend/app/api/endpoints/strategy_config.py
Normal file
165
backend/app/api/endpoints/strategy_config.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
전략 파라미터 관리 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 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]
|
||||
|
||||
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 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("/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 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):
|
||||
"""단일 파라미터를 기본값으로 리셋."""
|
||||
cfg = await strategy_cfg.reset_to_default(key)
|
||||
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
|
||||
Reference in New Issue
Block a user