feat: 데이트레이딩 조회 API를 실제 엔진 상태에 연결 + 대시보드 엔진 상태 바

/daytrading/{status,watchlist,positions}(v1)는 main.py가 실행하는 엔진
(services/daytrading)과 공유되지 않는 별개 인메모리 dict를 읽어 항상 0을 반환했음.
조회는 entry_service 감시목록·day_trading_state 기준으로 응답하고, POST /watchlist는
엔진에도 반영. 프론트 보유종목 패널 하단에 감시/ORB/포지션/청산/당일손익·현재 단계
(ORB 수집중·신호 대기·쿨다운 등)를 30초 폴링으로 표시.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 11:23:13 +09:00
parent 8b8c7a423e
commit b2a1b884d4
3 changed files with 133 additions and 24 deletions

View File

@@ -0,0 +1,81 @@
/**
* DayTradingStatusBar.tsx — 데이트레이딩 엔진 상태 요약 바
*
* 보유 종목 패널 하단에 붙어, 실제 매매 엔진(services/daytrading)의
* 감시 종목 수 / ORB 수집 수 / 활성 포지션 / 당일 손익 / 진입 가능 여부를 보여준다.
* 데이터: GET /daytrading/status (엔진 상태 기준으로 응답하도록 수정된 v1 엔드포인트)
* 폴링: 30초 (인메모리 조회라 가벼움)
*/
import React from 'react'
import { useQuery } from '@tanstack/react-query'
import { Zap } from 'lucide-react'
import api from '../lib/api'
interface DayTradingStatus {
watchlist_count: number
active_positions: number
orb_collected: number
daily_pnl: number
closed_today?: number
cooldown_active?: boolean
is_market_open: boolean
is_opening_range: boolean
is_entry_window: boolean
is_eod_flat: boolean
today: string
}
/** 현재 엔진 단계 라벨 — 사용자가 "왜 안 사는지"를 한눈에 보게 */
const phaseLabel = (s: DayTradingStatus): { text: string; color: string } => {
if (!s.is_market_open) return { text: '장 마감', color: 'text-[#4a6580]' }
if (s.is_eod_flat) return { text: 'EOD 청산', color: 'text-[#f97316]' }
if (s.is_opening_range) return { text: 'ORB 수집중', color: 'text-[#60a5fa]' }
if (s.cooldown_active) return { text: '3연패 쿨다운', color: 'text-[#ef4444]' }
if (!s.is_entry_window) return { text: '신규진입 마감', color: 'text-[#4a6580]' }
if (s.watchlist_count === 0) return { text: '감시종목 없음', color: 'text-[#ef4444]' }
return { text: '신호 대기', color: 'text-[#22c55e]' }
}
const DayTradingStatusBar = React.memo(() => {
const { data } = useQuery<DayTradingStatus>({
queryKey: ['daytrading-status'],
queryFn: async () => (await api.get('/daytrading/status')).data,
refetchInterval: 30000,
staleTime: 15000,
})
if (!data) return null
const phase = phaseLabel(data)
const pnl = data.daily_pnl ?? 0
const pnlColor = pnl > 0 ? 'text-[#ef4444]' : pnl < 0 ? 'text-[#3b82f6]' : 'text-[#8faabf]'
return (
<div className="px-3 py-1.5 border-t border-[#152035] bg-[#091018] flex-shrink-0">
<div className="flex items-center gap-1.5 mb-1">
<Zap className="w-2.5 h-2.5 text-[#f97316]" />
<span className="text-[9px] font-semibold uppercase tracking-wider text-[#f97316]">데이트레이딩 엔진</span>
<span className={`ml-auto text-[9px] font-mono ${phase.color}`}>{phase.text}</span>
</div>
<div className="grid grid-cols-4 gap-1 text-center">
{[
{ label: '감시', value: data.watchlist_count },
{ label: 'ORB', value: data.orb_collected },
{ label: '포지션', value: data.active_positions },
{ label: '청산', value: data.closed_today ?? 0 },
].map((c) => (
<div key={c.label} className="bg-[#0c1522] rounded-sm py-0.5">
<div className="text-[8px] text-[#4a6580]">{c.label}</div>
<div className="text-[10px] font-mono text-[#c9d7e5]">{c.value}</div>
</div>
))}
</div>
<div className="mt-1 flex justify-between text-[8px] font-mono">
<span className="text-[#4a6580]">당일 손익</span>
<span className={pnlColor}>{pnl.toLocaleString('ko-KR')}원</span>
</div>
</div>
)
})
DayTradingStatusBar.displayName = 'DayTradingStatusBar'
export default DayTradingStatusBar