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:
2026-03-03 10:40:02 +09:00
parent 14e6d87025
commit f3323bb742
4 changed files with 556 additions and 61 deletions

View File

@@ -4,7 +4,11 @@ API 라우터 통합 모듈
- 각 엔드포인트 라우터를 prefix/tag와 함께 등록
"""
from fastapi import APIRouter
from app.api.endpoints import trading_rules, trades, portfolio, market_data, mcp, recommendations, monitoring, tensorflow_predictions, debug, risk_management, ml_ensemble, journals, day_trading
from app.api.endpoints import (
trading_rules, trades, portfolio, market_data, mcp, recommendations,
monitoring, tensorflow_predictions, debug, risk_management, ml_ensemble,
journals, day_trading, strategy_config,
)
api_router = APIRouter()
@@ -21,3 +25,4 @@ api_router.include_router(risk_management.router, prefix="/risk", tags=["risk_ma
api_router.include_router(ml_ensemble.router, prefix="/ml-ensemble", tags=["ml_ensemble"])
api_router.include_router(journals.router, prefix="/journals", tags=["journals"])
api_router.include_router(day_trading.router, prefix="/daytrading", tags=["day_trading"])
api_router.include_router(strategy_config.router, prefix="/strategy-config", tags=["strategy_config"])

View 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