## CRITICAL 수정 (20건) - day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가 - trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산 - risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수 - risk_management/recommendations: date.today() → KST 변환 - mdd_manager: Lock 밖 executed_key 참조 수정 - correlation_filter: output → output2 키 수정 - portfolio: 해외 종목 USD/KRW 환율 적용 분리 - Sharpe Ratio 거래빈도 기반 연간화 - telegram_notifier: send_message() 공개 래퍼 추가 - position_manager: 앱 재시작 시 DB 포지션 복원 - swing exit_service: remaining_pct 조건 범위 수정 - run_backtest: original_qty 기반 remaining_pct 계산 - us_market_schedule: 2027년 미국 공휴일 추가 - Login.tsx: res.ok 검증 추가 - mean_reversion_strategy: RSI 로그 하드코딩 수정 ## HIGH 수정 (10건) - db_worker: DLQ 파일 백업 메커니즘 추가 - korea_investment_api: 만료 토큰 임의 연장 제거 - market_data: chart_cache 최대 100개 + TTL 만료 - main.py: rate_limit_store 1시간 미접근 IP 정리 - recommendations: fire-and-forget try/except 추가 - swing/scheduler: DB 세션 3개 분리 - Dashboard: wsMessages 전체 순회 - Layout: /health 엔드포인트 + 30초 폴링 - ml_ensemble: POST /train require_api_key 추가 - day_trading: record_trade_result_safe() 전환 ## 코드 정리 - 프론트엔드: isUsStock, formatTime 공통 유틸 추출 - 백엔드: dead code 제거, 중복 import 통합 - MCP: JSON 파싱 헬퍼 추출 (~60줄 감소) - 전체 ~100개 파일 한국어 주석 추가 - 테스트 13건 코드 기준 수정 (468 passed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
6.3 KiB
Python
150 lines
6.3 KiB
Python
"""
|
|
WebSocket 연결 관리 모듈
|
|
|
|
- 클라이언트 연결/해제 및 인증 관리
|
|
- 종목별 구독(subscribe/unsubscribe/set_subscriptions) 관리
|
|
- 개별 클라이언트, 전체, 종목 구독자 타겟 메시지 전송
|
|
- 싱글톤 websocket_manager 인스턴스 제공 (main.py 에서 import)
|
|
"""
|
|
from typing import Dict, Set
|
|
from fastapi import WebSocket
|
|
from app.core.json_helpers import _dumps
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WebSocketManager:
|
|
def __init__(self):
|
|
self.active_connections: Dict[str, WebSocket] = {}
|
|
# 클라이언트별 구독 종목 세트
|
|
self._subscriptions: Dict[str, Set[str]] = {}
|
|
|
|
# ------------------------------------------------------------------
|
|
# 연결 관리
|
|
# ------------------------------------------------------------------
|
|
async def connect(self, websocket: WebSocket, client_id: str):
|
|
"""클라이언트 연결.
|
|
|
|
동일 client_id로 재접속 시 active_connections를 교체.
|
|
이전 핸들러는 websocket_endpoint의 루프에서 교체 감지 후 자연 종료.
|
|
구독 목록은 유지 (새 연결이 즉시 실시간 데이터를 수신할 수 있도록).
|
|
"""
|
|
if client_id in self.active_connections:
|
|
logger.info(f"Client {client_id}: 기존 연결 교체 (이전 핸들러 자연 종료 대기)")
|
|
|
|
await websocket.accept()
|
|
self.active_connections[client_id] = websocket
|
|
self._subscriptions.setdefault(client_id, set())
|
|
logger.info(f"Client {client_id} connected. Total: {len(self.active_connections)}")
|
|
|
|
def disconnect(self, client_id: str):
|
|
"""클라이언트 연결 해제"""
|
|
self.active_connections.pop(client_id, None)
|
|
self._subscriptions.pop(client_id, None)
|
|
logger.info(f"Client {client_id} disconnected. Total: {len(self.active_connections)}")
|
|
|
|
# ------------------------------------------------------------------
|
|
# 구독 관리
|
|
# ------------------------------------------------------------------
|
|
def subscribe(self, client_id: str, stock_codes: list[str]):
|
|
"""클라이언트에 종목 추가 구독."""
|
|
bucket = self._subscriptions.setdefault(client_id, set())
|
|
bucket.update(stock_codes)
|
|
logger.debug(f"[sub+] {client_id}: {stock_codes} → 현재 {sorted(bucket)}")
|
|
|
|
def unsubscribe(self, client_id: str, stock_codes: list[str]):
|
|
"""클라이언트의 특정 종목 구독 해제."""
|
|
bucket = self._subscriptions.get(client_id, set())
|
|
bucket.difference_update(stock_codes)
|
|
logger.debug(f"[sub-] {client_id}: {stock_codes} → 현재 {sorted(bucket)}")
|
|
|
|
def set_subscriptions(self, client_id: str, stock_codes: list[str]):
|
|
"""클라이언트의 구독 목록을 전체 교체."""
|
|
self._subscriptions[client_id] = set(stock_codes)
|
|
logger.debug(f"[sub=] {client_id}: 구독 목록 재설정 → {sorted(stock_codes)}")
|
|
|
|
def get_subscriptions(self, client_id: str) -> list[str]:
|
|
"""클라이언트의 현재 구독 목록 반환."""
|
|
return sorted(self._subscriptions.get(client_id, set()))
|
|
|
|
def get_all_subscribed_codes(self) -> Set[str]:
|
|
"""모든 활성 클라이언트에 걸쳐 구독 중인 종목 합집합 반환.
|
|
KisRealtimeStream이 이 목록을 KIS 측에 구독한다.
|
|
"""
|
|
result: Set[str] = set()
|
|
for client_id in self.active_connections:
|
|
result.update(self._subscriptions.get(client_id, set()))
|
|
return result
|
|
|
|
# ------------------------------------------------------------------
|
|
# 메시지 전송
|
|
# ------------------------------------------------------------------
|
|
async def send_personal_message(self, message: str, client_id: str):
|
|
"""특정 클라이언트에게 텍스트 메시지 전송"""
|
|
ws = self.active_connections.get(client_id)
|
|
if ws:
|
|
try:
|
|
await ws.send_text(message)
|
|
except Exception as e:
|
|
logger.error(f"Error sending to {client_id}: {e}")
|
|
self.disconnect(client_id)
|
|
|
|
async def broadcast(self, message: str):
|
|
"""모든 연결된 클라이언트에게 메시지 브로드캐스트"""
|
|
disconnected = []
|
|
for client_id, ws in list(self.active_connections.items()):
|
|
try:
|
|
await ws.send_text(message)
|
|
except Exception as e:
|
|
logger.error(f"Error broadcasting to {client_id}: {e}")
|
|
disconnected.append(client_id)
|
|
for client_id in disconnected:
|
|
self.disconnect(client_id)
|
|
|
|
async def broadcast_json(self, data: dict):
|
|
"""JSON 데이터를 모든 클라이언트에게 브로드캐스트"""
|
|
await self.broadcast(_dumps(data))
|
|
|
|
async def send_market_update(self, stock_code: str, data: dict):
|
|
"""시장 데이터를 해당 종목을 구독한 클라이언트에게만 전송.
|
|
구독자가 없으면 전체 브로드캐스트(기존 동작 유지).
|
|
"""
|
|
message = _dumps({
|
|
"type": "market_update",
|
|
"stock_code": stock_code,
|
|
"data": data,
|
|
})
|
|
# 해당 종목 구독 클라이언트 필터링
|
|
targets = [
|
|
cid for cid, codes in self._subscriptions.items()
|
|
if stock_code in codes and cid in self.active_connections
|
|
]
|
|
if not targets:
|
|
# 구독자 없음 — 전체 브로드캐스트(fallback)
|
|
await self.broadcast(message)
|
|
return
|
|
disconnected = []
|
|
for cid in targets:
|
|
ws = self.active_connections.get(cid)
|
|
if not ws:
|
|
continue
|
|
try:
|
|
await ws.send_text(message)
|
|
except Exception as e:
|
|
logger.error(f"send_market_update error → {cid}: {e}")
|
|
disconnected.append(cid)
|
|
for cid in disconnected:
|
|
self.disconnect(cid)
|
|
|
|
async def send_trade_notification(self, trade_data: dict):
|
|
"""거래 알림 전송"""
|
|
await self.broadcast_json({"type": "trade_notification", "data": trade_data})
|
|
|
|
async def send_mcp_status(self, status: dict):
|
|
"""MCP 상태 업데이트 전송"""
|
|
await self.broadcast_json({"type": "mcp_status", "data": status})
|
|
|
|
|
|
websocket_manager = WebSocketManager()
|