/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>
82 lines
3.6 KiB
TypeScript
82 lines
3.6 KiB
TypeScript
/**
|
|
* 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
|