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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user