Files
auto-stock/backend/app/api/endpoints/strategy_config.py
shuim a264561576 fix: 하드닝 루프 8회차 — 체결 보정·고아 감지·청산 경보·실시간/차트·리스크 가드 일괄 수정
- 스윙 체결가 확인 전 executed_price NULL + fill_sweep, 진입가 resolve(ActiveTrade↔Portfolio), 0체결/부분체결 매수 장부 원복
- DT 고아 감지(근거 한정)·0체결 매도 복원·stale 점검 확대, 시간손절 -0.3% 원복
- 폴백 손절 ATR 캐시, 거래량부족-차단 신호 매수 차단, 현금 5% 유보 재적용, MDD peak/reset 수정
- KIS 서킷브레이커 청산 우회·경보, db_worker 재큐잉 순서, heartbeat·health-watch 사이드카
- 실시간 WS 재구독·41건 우선순위·다건 프레임 분할, 차트 샘플 제거·당일봉 합성, WS 서브프로토콜 인증
- 프론트: 미관리 그룹·확인 모달·차트 실시간 반영·예수금 표시 정정
- 전문가 에이전트 6종 추가(.claude/agents)
- 유닛 스위트 2667 passed (소켓 차단 하), tsc OK

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y83Za98XsNw9zuA4FTUytN
2026-10-10 12:17:49 +09:00

180 lines
6.4 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 (
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