[추가 파일]
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: 평균회귀 전략 파라미터
166 lines
6.0 KiB
Python
166 lines
6.0 KiB
Python
"""
|
|
전략 파라미터 관리 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
|