fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리

## 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>
This commit is contained in:
2026-03-26 16:05:41 +09:00
parent 739a8a2e5d
commit de7921d270
149 changed files with 133353 additions and 3338 deletions

View File

@@ -1,65 +1,115 @@
/**
* useMarketData.ts — 실시간 시장 데이터 관리 커스텀 훅
*
* 역할:
* - WebSocket tick/quote 메시지를 수신하여 종목별 실시간 가격 관리
* - 선택 종목의 호가 데이터(liveQuote) 관리
* - 포트폴리오 초기 가격 설정 (WebSocket 데이터 수신 전 폴백)
*
* 성능 최적화 패턴 (ref + 주기적 스냅샷):
* - tick 수신 시 ref에만 저장 (리렌더 없음)
* - 1초마다 dirty 체크 후 변경분만 state 반영
* - Dashboard 리렌더를 초당 최대 1회로 제한하여 깜빡임 방지
*
* @param selectedStock - 현재 선택된 종목 코드 (호가 필터링용)
* @param portfolio - 보유 종목 배열 (초기 가격 설정용)
* @returns livePrices, livePricesRef, liveQuote, handleMarketUpdate
*/
import { useEffect, useState, useRef, useCallback } from 'react'
export const useMarketData = (
selectedStock: string,
portfolio: any[] | undefined
) => {
// Live prices with throttling to prevent excessive re-renders
// === livePrices: ref + 주기적 스냅샷 패턴 ===
// tick마다 ref에만 저장 → 1초마다 변경된 경우에만 state 업데이트
// Dashboard 리렌더를 초당 최대 1회로 제한 (깜빡임 방지)
const [livePrices, setLivePrices] = useState<Record<string, number>>({})
/** 실시간 가격을 즉시 저장하는 ref (리렌더 없이 최신 가격 유지) */
const livePricesRef = useRef<Record<string, number>>({})
/** 가격 변경 여부 플래그 — flush interval에서 확인 */
const pricesDirtyRef = useRef(false)
/** 선택 종목의 실시간 호가 데이터 (WebSocket quote 메시지) */
const [liveQuote, setLiveQuote] = useState<any>(null)
// Throttle ref: track last update time per stock
const lastPriceUpdateRef = useRef<Record<string, number>>({})
const throttleDelayMs = 500 // Max 2-3 price updates per second (500ms = 2/sec)
// 1초마다 변경된 가격을 ref에서 state로 flush
useEffect(() => {
const id = setInterval(() => {
if (pricesDirtyRef.current) {
pricesDirtyRef.current = false
setLivePrices({ ...livePricesRef.current })
}
}, 1000)
return () => clearInterval(id)
}, [])
// Handle market updates (tick data) with throttling
/**
* WebSocket market_update 메시지 핸들러
*
* tick 메시지: 체결가 업데이트 (ref에만 저장, 리렌더 없음)
* quote 메시지: 호가 업데이트
* - bid1(최우선 매수호가)을 가격으로 사용 (tick 없을 때 P&L 계산용)
* - 선택 종목이면 liveQuote state 업데이트 (호가창 갱신)
*/
const handleMarketUpdate = useCallback((data: unknown) => {
const d = data as any
if (d?._type === 'tick' && d.stock_code && d.price != null) {
const now = Date.now()
const lastUpdate =
lastPriceUpdateRef.current[d.stock_code] ?? 0
// Only update if throttle delay has passed
if (now - lastUpdate >= throttleDelayMs) {
setLivePrices((prev) => ({
...prev,
[d.stock_code]: d.price,
}))
lastPriceUpdateRef.current[d.stock_code] = now
// 체결가 변경 시에만 ref 업데이트 + dirty 플래그 설정
const prev = livePricesRef.current[d.stock_code]
if (prev !== d.price) {
livePricesRef.current[d.stock_code] = d.price
pricesDirtyRef.current = true
}
} else if (d?._type === 'quote' && d.stock_code) {
// 호가 데이터에서 현재가 추정 (bid1 = 최우선 매수호가)
// tick 데이터가 없을 때 P&L 계산용 가격 업데이트
const bidPrice = d.bid1 as number | undefined
if (bidPrice && bidPrice > 0) {
const prev = livePricesRef.current[d.stock_code]
if (prev !== bidPrice) {
livePricesRef.current[d.stock_code] = bidPrice
pricesDirtyRef.current = true
}
}
// 선택된 종목이면 호가창 업데이트 (OrderbookPanel 갱신)
if (d.stock_code === selectedStock) {
setLiveQuote(d)
}
} else if (
d?._type === 'quote' &&
d.stock_code === selectedStock
) {
setLiveQuote(d)
}
}, [selectedStock])
// Initialize live prices from portfolio
// 포트폴리오 로드 시 초기 가격 설정 (WebSocket 연결 전 가격 폴백)
useEffect(() => {
if (!portfolio) return
setLivePrices((prev) => {
const next = { ...prev }
portfolio.forEach((s: any) => {
if (s.current_price && !next[s.stock_code]) {
next[s.stock_code] = Number(s.current_price)
}
})
return next
let changed = false
portfolio.forEach((s: any) => {
// 아직 실시간 가격이 없는 종목만 초기화
if (s.current_price && !livePricesRef.current[s.stock_code]) {
livePricesRef.current[s.stock_code] = Number(s.current_price)
changed = true
}
})
if (changed) {
setLivePrices({ ...livePricesRef.current })
}
}, [portfolio])
// Clear live quote when selected stock changes
// 선택 종목 변경 시 이전 호가 데이터 초기화
useEffect(() => {
setLiveQuote(null)
}, [selectedStock])
return {
/** 종목별 실시간 가격 맵 (state — 1초마다 갱신) */
livePrices,
/** 종목별 실시간 가격 ref (리렌더 없이 즉시 접근) */
livePricesRef,
/** 선택 종목 실시간 호가 데이터 */
liveQuote,
/** WebSocket market_update 메시지 콜백 */
handleMarketUpdate,
}
}