가상 수정
This commit is contained in:
@@ -3,24 +3,28 @@ import type { ConditionStatus } from '../../utils/virtualSignalMetrics';
|
||||
|
||||
interface Props {
|
||||
status: ConditionStatus;
|
||||
/** 조건 매칭율 0~100 */
|
||||
matchRate?: number | null;
|
||||
/** @deprecated matchRate 사용 */
|
||||
progress?: number | null;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const LABELS: Record<ConditionStatus, string> = {
|
||||
match: 'Match',
|
||||
pending: 'Pending',
|
||||
nomatch: 'Mismatch',
|
||||
match: '충족',
|
||||
pending: '대기',
|
||||
nomatch: '미충족',
|
||||
unknown: '—',
|
||||
};
|
||||
|
||||
const ConditionStatusSignal: React.FC<Props> = ({ status, progress, compact = false }) => {
|
||||
const ConditionStatusSignal: React.FC<Props> = ({ status, matchRate, progress, compact = false }) => {
|
||||
if (status === 'unknown') {
|
||||
return <span className="vtd-cond-signal vtd-cond-signal--unknown">—</span>;
|
||||
}
|
||||
|
||||
const pct = progress != null && Number.isFinite(progress)
|
||||
? `${Math.round(progress)}%`
|
||||
const rate = matchRate ?? progress;
|
||||
const pct = rate != null && Number.isFinite(rate)
|
||||
? `${Math.round(rate)}%`
|
||||
: null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,26 +1,70 @@
|
||||
import React from 'react';
|
||||
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
|
||||
import ConditionStatusSignal from './ConditionStatusSignal';
|
||||
import { resolveHeatTier } from '../../utils/virtualSignalMetrics';
|
||||
|
||||
interface Props {
|
||||
metrics: ConditionMetric[];
|
||||
}
|
||||
|
||||
const IconMismatch = () => (
|
||||
<svg className="vtd-heat-icon-svg" width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconPending = () => (
|
||||
<svg className="vtd-heat-icon-svg" width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M12 9v4" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconMatch = () => (
|
||||
<svg className="vtd-heat-icon-svg" width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<polyline points="20 6 9 17 4 12" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const VirtualConditionList: React.FC<Props> = ({ metrics }) => {
|
||||
if (metrics.length === 0) {
|
||||
return <p className="vtd-cond-list-empty vtd-muted">조건 없음</p>;
|
||||
return <p className="vtd-heat-list-empty vtd-muted">조건 없음</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="vtd-cond-list" aria-label="전략 조건 목록">
|
||||
{metrics.map(({ row, status, progress }) => (
|
||||
<li key={row.id} className="vtd-cond-list-item">
|
||||
<span className="vtd-cond-list-name" title={row.displayName}>
|
||||
{row.displayName}
|
||||
</span>
|
||||
<ConditionStatusSignal status={status} progress={progress} compact />
|
||||
</li>
|
||||
))}
|
||||
<ul className="vtd-heat-list" aria-label="전략 지표 매치율">
|
||||
{metrics.map(({ row, status, matchRate }) => {
|
||||
const tier = resolveHeatTier(matchRate, status);
|
||||
const pct = Math.min(100, Math.max(0, matchRate));
|
||||
const minFill = pct > 0 && tier === 'nomatch' ? 6 : pct > 0 ? 6 : 0;
|
||||
return (
|
||||
<li key={row.id} className={`vtd-heat-row vtd-heat-row--${tier}`}>
|
||||
<div
|
||||
className="vtd-heat-track"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={pct}
|
||||
aria-label={`${row.displayName} 매치율 ${pct}%`}
|
||||
>
|
||||
<span className="vtd-heat-label" title={row.displayName}>
|
||||
{row.displayName}
|
||||
</span>
|
||||
<div className="vtd-heat-fill" style={{ width: `${Math.max(pct, minFill)}%` }} />
|
||||
<span className="vtd-heat-pct">{pct}%</span>
|
||||
</div>
|
||||
<span className="vtd-heat-icon" aria-hidden>
|
||||
{tier === 'match' ? <IconMatch /> : tier === 'pending' ? <IconPending /> : <IconMismatch />}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
|
||||
import { formatStrategyThreshold } from '../../utils/virtualSignalMetrics';
|
||||
import { formatStrategyThreshold, resolveHeatTier } from '../../utils/virtualSignalMetrics';
|
||||
import { formatIndicatorValue } from '../../utils/virtualStrategyConditions';
|
||||
import ConditionStatusSignal from './ConditionStatusSignal';
|
||||
|
||||
@@ -10,19 +10,28 @@ interface Props {
|
||||
|
||||
const VirtualIndicatorCompareTable: React.FC<Props> = ({ metrics }) => (
|
||||
<div className="vtd-sig-table-wrap">
|
||||
<div className="vtd-sig-table-head">REAL-TIME INDICATOR COMPARISON</div>
|
||||
<div className="vtd-sig-table-head">실시간 지표 비교</div>
|
||||
<table className="vtd-sig-table">
|
||||
<colgroup>
|
||||
<col className="vtd-sig-col vtd-sig-col--ind" />
|
||||
<col className="vtd-sig-col vtd-sig-col--val" />
|
||||
<col className="vtd-sig-col vtd-sig-col--threshold" />
|
||||
<col className="vtd-sig-col vtd-sig-col--progress" />
|
||||
<col className="vtd-sig-col vtd-sig-col--status" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TECHNICAL INDICATOR</th>
|
||||
<th>CURRENT VALUE</th>
|
||||
<th>STRATEGY THRESHOLD</th>
|
||||
<th>PROGRESS</th>
|
||||
<th>STATUS</th>
|
||||
<th className="vtd-sig-table-th--ind">기술적 지표</th>
|
||||
<th className="vtd-sig-table-th--val">현재값</th>
|
||||
<th className="vtd-sig-table-th--threshold">전략 조건</th>
|
||||
<th className="vtd-sig-table-th--progress">진행률</th>
|
||||
<th className="vtd-sig-table-th--status">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{metrics.map(({ row, status, progress }) => (
|
||||
{metrics.map(({ row, status, matchRate }) => {
|
||||
const tier = resolveHeatTier(matchRate, status);
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="vtd-sig-table-ind">{row.displayName}</td>
|
||||
<td className="vtd-sig-table-val">{formatIndicatorValue(row.currentValue)}</td>
|
||||
@@ -31,17 +40,20 @@ const VirtualIndicatorCompareTable: React.FC<Props> = ({ metrics }) => (
|
||||
?? formatStrategyThreshold(row.conditionType, row.targetValue, row.conditionLabel)}
|
||||
</td>
|
||||
<td className="vtd-sig-table-progress">
|
||||
<div className="vtd-sig-row-bar">
|
||||
<div className="vtd-sig-row-bar vtd-sig-row-bar--heat">
|
||||
<div
|
||||
className={`vtd-sig-row-bar-fill vtd-sig-row-bar-fill--${status}`}
|
||||
style={{ width: `${progress ?? 0}%` }}
|
||||
className={`vtd-sig-row-bar-fill vtd-sig-row-bar-fill--${tier}`}
|
||||
style={{ width: `${matchRate}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="vtd-sig-row-pct">{progress != null ? `${Math.round(progress)}%` : '—'}</span>
|
||||
<span className="vtd-sig-table-pct">{`${matchRate}%`}</span>
|
||||
</td>
|
||||
<td className="vtd-sig-table-status-cell">
|
||||
<ConditionStatusSignal status={status} matchRate={matchRate} compact />
|
||||
</td>
|
||||
<td><ConditionStatusSignal status={status} progress={progress} /></td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
|
||||
import VirtualTargetCardChart from './VirtualTargetCardChart';
|
||||
import VirtualTargetSignalPanel from './VirtualTargetSignalPanel';
|
||||
import type { Theme } from '../../types';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
import VirtualTargetQuote from './VirtualTargetQuote';
|
||||
|
||||
export type VirtualCardDisplayMode = 'signal' | 'chart';
|
||||
|
||||
@@ -34,6 +36,7 @@ interface Props {
|
||||
theme?: Theme;
|
||||
chartRealtimeSource?: ChartRealtimeSource;
|
||||
chartSeriesPriceLabels?: boolean;
|
||||
ticker?: TickerData;
|
||||
}
|
||||
|
||||
const VirtualTargetCard: React.FC<Props> = ({
|
||||
@@ -56,6 +59,7 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
theme = 'dark',
|
||||
chartRealtimeSource = 'BACKEND_STOMP',
|
||||
chartSeriesPriceLabels = true,
|
||||
ticker,
|
||||
}) => {
|
||||
const ko = getKoreanName(market);
|
||||
const sym = market.replace(/^KRW-/, '');
|
||||
@@ -122,6 +126,7 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<span className="vtd-card-tf">시간봉 {tf}</span>
|
||||
</div>
|
||||
<div className="vtd-card-head-actions">
|
||||
{onEnterFocus && (
|
||||
@@ -162,7 +167,7 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
</div>
|
||||
|
||||
<div className="vtd-card-meta">
|
||||
<span className="vtd-card-tf">시간봉 {tf}</span>
|
||||
<VirtualTargetQuote market={market} ticker={ticker} compact />
|
||||
{!isChart && isDetail && evalCount > 0 && (
|
||||
<span className="vtd-card-met-count">{metCount}/{evalCount} 조건 충족</span>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,9 @@ import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
|
||||
import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
|
||||
import type { Theme } from '../../types';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
import VirtualLiveBadge from './VirtualLiveBadge';
|
||||
import VirtualTargetQuote from './VirtualTargetQuote';
|
||||
import VirtualTargetCardChart from './VirtualTargetCardChart';
|
||||
import VirtualTargetSignalPanel from './VirtualTargetSignalPanel';
|
||||
|
||||
@@ -27,6 +29,7 @@ interface Props {
|
||||
theme?: Theme;
|
||||
chartRealtimeSource?: ChartRealtimeSource;
|
||||
chartSeriesPriceLabels?: boolean;
|
||||
ticker?: TickerData;
|
||||
}
|
||||
|
||||
/** 전체보기 — 좌 차트 · 우 신호 */
|
||||
@@ -46,6 +49,7 @@ const VirtualTargetFocusView: React.FC<Props> = ({
|
||||
theme = 'dark',
|
||||
chartRealtimeSource = 'BACKEND_STOMP',
|
||||
chartSeriesPriceLabels = true,
|
||||
ticker,
|
||||
}) => {
|
||||
const ko = getKoreanName(market);
|
||||
const sym = market.replace(/^KRW-/, '');
|
||||
@@ -100,6 +104,10 @@ const VirtualTargetFocusView: React.FC<Props> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="vtd-focus-meta">
|
||||
<VirtualTargetQuote market={market} ticker={ticker} compact />
|
||||
</div>
|
||||
|
||||
<div className="vtd-focus-split">
|
||||
<section className="vtd-focus-pane vtd-focus-pane--chart" aria-label="차트 보기">
|
||||
<div className="vtd-focus-pane-head">차트</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { VirtualTargetItem, VirtualCardViewMode, VirtualSessionConfig } fro
|
||||
import { loadVirtualCardViewMode, saveVirtualCardViewMode } from '../../utils/virtualTradingStorage';
|
||||
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
|
||||
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
|
||||
interface Props {
|
||||
targets: VirtualTargetItem[];
|
||||
@@ -28,6 +29,7 @@ interface Props {
|
||||
theme?: Theme;
|
||||
chartRealtimeSource?: ChartRealtimeSource;
|
||||
chartSeriesPriceLabels?: boolean;
|
||||
tickers?: Map<string, TickerData>;
|
||||
}
|
||||
|
||||
const VirtualTargetGrid: React.FC<Props> = ({
|
||||
@@ -39,6 +41,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
theme = 'dark',
|
||||
chartRealtimeSource = 'BACKEND_STOMP',
|
||||
chartSeriesPriceLabels = true,
|
||||
tickers,
|
||||
}) => {
|
||||
const [viewMode, setViewMode] = useState<VirtualCardViewMode>(() => loadVirtualCardViewMode());
|
||||
const [globalDisplayMode, setGlobalDisplayMode] = useState<VirtualCardDisplayMode>('signal');
|
||||
@@ -137,6 +140,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
theme={theme}
|
||||
chartRealtimeSource={chartRealtimeSource}
|
||||
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
||||
ticker={tickers?.get(focusTarget.market)}
|
||||
/>
|
||||
) : (
|
||||
<div className="vtd-grid">
|
||||
@@ -164,6 +168,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
theme={theme}
|
||||
chartRealtimeSource={chartRealtimeSource}
|
||||
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
||||
ticker={tickers?.get(t.market)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -25,9 +25,11 @@ const ChangeArrow = memo(function ChangeArrow({ change }: { change: ChangeDir })
|
||||
interface Props {
|
||||
market: string;
|
||||
ticker?: TickerData;
|
||||
/** 카드 메타 행 — 현재가만 간략 표시 */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const VirtualTargetQuote: React.FC<Props> = ({ market, ticker }) => {
|
||||
const VirtualTargetQuote: React.FC<Props> = ({ market, ticker, compact = false }) => {
|
||||
const code = market.replace(/^KRW-/, '');
|
||||
const change: ChangeDir = ticker?.change ?? 'EVEN';
|
||||
const colorCls = change === 'RISE' ? 'vtd-target-up' : change === 'FALL' ? 'vtd-target-dn' : 'vtd-target-even';
|
||||
@@ -49,6 +51,20 @@ const VirtualTargetQuote: React.FC<Props> = ({ market, ticker }) => {
|
||||
prevPriceRef.current = price;
|
||||
}, [ticker?.tradePrice]);
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`vtd-target-quote vtd-target-quote--compact ${flashClass}`.trim()} aria-label="현재가">
|
||||
<span className="vtd-card-price-label">현재가</span>
|
||||
<span className={`vtd-target-price ${colorCls}`}>
|
||||
{ticker ? fmtKrw(ticker.tradePrice) : '-'}
|
||||
</span>
|
||||
<span className={`vtd-target-change ${colorCls}`}>
|
||||
{ticker ? fmtPct(ticker.changeRate) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`vtd-target-quote ${flashClass}`.trim()} aria-label="현재가 및 전일 대비">
|
||||
<div className="vtd-target-quote-left">
|
||||
|
||||
@@ -34,8 +34,6 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
|
||||
[metrics, snapshot?.matchRate],
|
||||
);
|
||||
const trafficState = useMemo(() => getTrafficLightState(matchRate, metrics), [matchRate, metrics]);
|
||||
const metCount = metrics.filter(m => m.status === 'match').length;
|
||||
const evalCount = metrics.filter(m => m.status !== 'unknown').length;
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
@@ -49,17 +47,12 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
|
||||
<div
|
||||
className={[
|
||||
'vtd-sig-panel-wrap',
|
||||
isDetail ? '' : 'vtd-sig-panel-wrap--summary',
|
||||
isDetail ? 'vtd-sig-panel-wrap--detail' : 'vtd-sig-panel-wrap--summary',
|
||||
receiving ? 'vtd-sig-panel-wrap--receiving' : '',
|
||||
className,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{isDetail && evalCount > 0 && (
|
||||
<div className="vtd-focus-sig-meta">
|
||||
<span className="vtd-card-met-count">{metCount}/{evalCount} 조건 충족</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={`vtd-sig-panel${isDetail ? '' : ' vtd-sig-panel--summary'}`}>
|
||||
<div className={`vtd-sig-panel${isDetail ? ' vtd-sig-panel--detail-top' : ' vtd-sig-panel--summary'}`}>
|
||||
<div className="vtd-sig-panel-title">SIGNAL INTELLIGENCE & MATCH RATES</div>
|
||||
<div className="vtd-sig-visual">
|
||||
<div className="vtd-sig-visual-eq">
|
||||
@@ -68,8 +61,12 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
|
||||
<VirtualConditionList metrics={metrics} />
|
||||
<VirtualSignalTrafficLight state={trafficState} matchRate={matchRate} />
|
||||
</div>
|
||||
{isDetail && <VirtualIndicatorCompareTable metrics={metrics} />}
|
||||
</div>
|
||||
{isDetail && (
|
||||
<div className="vtd-sig-detail-table" aria-label="지표별 상태 목록">
|
||||
<VirtualIndicatorCompareTable metrics={metrics} />
|
||||
</div>
|
||||
)}
|
||||
<div className="vtd-card-foot">
|
||||
<span className="vtd-card-updated">갱신 {formatUpdatedTime(snapshot?.updatedAt)}</span>
|
||||
<span className="vtd-card-match-summary">매매조건 일치율 {matchRate}%</span>
|
||||
|
||||
Reference in New Issue
Block a user