- 스윙 체결가 확인 전 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
180 lines
6.4 KiB
Python
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
|