가상투자 화면 목록 선택 동작

This commit is contained in:
Macbook
2026-05-25 14:05:13 +09:00
parent 20f11f6c4b
commit 3102169541
24 changed files with 130 additions and 27 deletions
+2
View File
@@ -25,6 +25,8 @@ const PERIODS: { label: string; tf: Timeframe }[] = [
{ label: '4h', tf: '4h' },
{ label: '1h', tf: '1h' },
{ label: '30m',tf: '30m' },
{ label: '15m',tf: '15m' },
{ label: '10m',tf: '10m' },
{ label: '5m', tf: '5m' },
{ label: '3m', tf: '3m' },
{ label: '1m', tf: '1m' },
+1 -1
View File
@@ -37,7 +37,7 @@ import type {
import type { ChartManager } from '../utils/ChartManager';
// ── 타임프레임 옵션 ────────────────────────────────────────────────────────
const TF_OPTIONS: Timeframe[] = ['1m','3m','5m','15m','30m','1h','4h','1D','1W','1M'];
const TF_OPTIONS: Timeframe[] = ['1m','3m','5m','10m','15m','30m','1h','4h','1D','1W','1M'];
// ── Ws 배지 ───────────────────────────────────────────────────────────────
const WsBadge: React.FC<{ status: WsStatus; isUpbit: boolean }> = ({ status, isUpbit }) => {
@@ -35,6 +35,7 @@ export const ALL_TIMEFRAMES: { tf: Timeframe; label: string }[] = [
{ tf: '1m', label: '1분' },
{ tf: '3m', label: '3분' },
{ tf: '5m', label: '5분' },
{ tf: '10m', label: '10분' },
{ tf: '15m', label: '15분' },
{ tf: '30m', label: '30분' },
{ tf: '1h', label: '1시간' },
+1 -1
View File
@@ -286,7 +286,7 @@ const ChartIcon: Record<ChartType, React.ReactNode> = {
baseline: <IcLine />,
};
const TIMEFRAMES: Timeframe[] = ['1m','3m','5m','15m','30m','1h','4h','1D','1W','1M'];
const TIMEFRAMES: Timeframe[] = ['1m','3m','5m','10m','15m','30m','1h','4h','1D','1W','1M'];
const CHART_TYPES: { value: ChartType; label: string }[] = [
{ value: 'candlestick', label: '캔들스틱' },
{ value: 'bar', label: '바' },
+18 -5
View File
@@ -110,12 +110,23 @@ const VirtualTradingPage: React.FC<Props> = ({
const tradePrice = tickers?.get(selectedMarket)?.tradePrice ?? null;
const handleSelectMarket = useCallback((market: string) => {
setSelectedMarket(market);
const price = tickers?.get(market)?.tradePrice;
if (price != null && price > 0) {
const seq = Date.now();
setFillBuy({ market, price, side: 'buy', seq });
setFillSell({ market, price, side: 'sell', seq: seq + 1 });
}
setRightTab('trade');
}, [tickers]);
const handleTargetsChange = useCallback((items: VirtualTargetItem[]) => {
setTargets(items);
if (items.length > 0 && !items.some(t => t.market === selectedMarket)) {
setSelectedMarket(items[0].market);
handleSelectMarket(items[0].market);
}
}, [selectedMarket]);
}, [selectedMarket, handleSelectMarket]);
const handleGlobalStrategyChange = useCallback((strategyId: number | null) => {
setSession(s => ({ ...s, globalStrategyId: strategyId }));
@@ -227,7 +238,7 @@ const VirtualTradingPage: React.FC<Props> = ({
selectedMarket={selectedMarket}
tickers={tickers}
onTargetsChange={handleTargetsChange}
onSelectMarket={setSelectedMarket}
onSelectMarket={handleSelectMarket}
onTargetStrategyChange={(market, strategyId) => void handleTargetStrategyChange(market, strategyId)}
/>
)}
@@ -245,6 +256,8 @@ const VirtualTradingPage: React.FC<Props> = ({
onTargetStrategyChange={(market, strategyId) => void handleTargetStrategyChange(market, strategyId)}
liveStatusByMarket={mergedLiveStatus}
lastTickAtByMarket={lastTickAtByMarket}
selectedMarket={selectedMarket}
onSelectMarket={handleSelectMarket}
theme={theme}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
@@ -267,7 +280,7 @@ const VirtualTradingPage: React.FC<Props> = ({
availableKrw={summary?.cashBalance ?? 0}
fillRequest={fillBuy}
searchAnchorRef={orderAnchorRef}
onMarketSelect={setSelectedMarket}
onMarketSelect={handleSelectMarket}
showSymbolField
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
@@ -283,7 +296,7 @@ const VirtualTradingPage: React.FC<Props> = ({
tradePrice={tradePrice}
availableCoinQty={posQty}
fillRequest={fillSell}
onMarketSelect={setSelectedMarket}
onMarketSelect={handleSelectMarket}
showSymbolField={false}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
@@ -3,7 +3,7 @@ import { createChart, CandlestickSeries, type IChartApi, type ISeriesApi, type T
import type { Theme, Timeframe } from '../../types';
import { fetchUpbitCandles } from '../../utils/upbitApi';
const TF_OPTIONS: Timeframe[] = ['1m', '5m', '15m', '1h', '4h', '1D'];
const TF_OPTIONS: Timeframe[] = ['1m', '5m', '10m', '15m', '1h', '4h', '1D'];
interface Props {
market: string;
@@ -4,7 +4,7 @@ import type { StrategyDto } from '../../utils/backendApi';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
import { buildConditionMetrics } from '../../utils/virtualSignalMetrics';
import { buildConditionMetrics, resolveVirtualTradeTiming } from '../../utils/virtualSignalMetrics';
import VirtualLiveBadge from './VirtualLiveBadge';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
@@ -29,6 +29,8 @@ interface Props {
displayMode?: VirtualCardDisplayMode;
onDisplayModeChange?: (mode: VirtualCardDisplayMode) => void;
onEnterFocus?: () => void;
selected?: boolean;
onSelect?: () => void;
theme?: Theme;
chartRealtimeSource?: ChartRealtimeSource;
chartSeriesPriceLabels?: boolean;
@@ -49,6 +51,8 @@ const VirtualTargetCard: React.FC<Props> = ({
displayMode = 'signal',
onDisplayModeChange,
onEnterFocus,
selected = false,
onSelect,
theme = 'dark',
chartRealtimeSource = 'BACKEND_STOMP',
chartSeriesPriceLabels = true,
@@ -59,6 +63,7 @@ const VirtualTargetCard: React.FC<Props> = ({
const rows = snapshot?.rows ?? [];
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
const tradeTiming = useMemo(() => resolveVirtualTradeTiming(metrics), [metrics]);
const metCount = metrics.filter(m => m.status === 'match').length;
const evalCount = metrics.filter(m => m.status !== 'unknown').length;
@@ -80,7 +85,20 @@ const VirtualTargetCard: React.FC<Props> = ({
receiving ? 'vtd-card--receiving' : '',
isDetail ? 'vtd-card--detail' : 'vtd-card--summary',
isChart ? 'vtd-card--chart-mode' : '',
selected ? 'vtd-card--selected' : '',
tradeTiming === 'buy' ? 'vtd-card--timing-buy' : '',
tradeTiming === 'sell' ? 'vtd-card--timing-sell' : '',
].filter(Boolean).join(' ')}
onClick={onSelect}
onKeyDown={e => {
if (onSelect && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
onSelect();
}
}}
role={onSelect ? 'button' : undefined}
tabIndex={onSelect ? 0 : undefined}
aria-pressed={onSelect ? selected : undefined}
>
<div className="vtd-card-head">
<div className="vtd-card-head-main">
@@ -122,7 +140,7 @@ const VirtualTargetCard: React.FC<Props> = ({
</svg>
</button>
)}
<div className="vtd-card-mode-toggle" role="group" aria-label="이 종목 표시">
<div className="vtd-card-mode-toggle" role="group" aria-label="이 종목 표시" onClick={e => e.stopPropagation()}>
<button
type="button"
className={`vtd-card-mode-btn${displayMode === 'signal' ? ' vtd-card-mode-btn--on' : ''}`}
@@ -23,6 +23,8 @@ interface Props {
onTargetStrategyChange: (market: string, strategyId: number | null) => void;
liveStatusByMarket?: Record<string, VirtualLiveStatus>;
lastTickAtByMarket?: Record<string, number>;
selectedMarket?: string;
onSelectMarket?: (market: string) => void;
theme?: Theme;
chartRealtimeSource?: ChartRealtimeSource;
chartSeriesPriceLabels?: boolean;
@@ -32,7 +34,9 @@ const VirtualTargetGrid: React.FC<Props> = ({
targets, strategies, snapshots, session,
onStrategyChange, onExecutionTypeChange, onPositionModeChange, onStart, onStop,
onTargetStrategyChange,
liveStatusByMarket = {}, lastTickAtByMarket = {}, theme = 'dark',
liveStatusByMarket = {}, lastTickAtByMarket = {}, selectedMarket,
onSelectMarket,
theme = 'dark',
chartRealtimeSource = 'BACKEND_STOMP',
chartSeriesPriceLabels = true,
}) => {
@@ -155,6 +159,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
displayMode={resolveCardDisplayMode(t.market)}
onDisplayModeChange={mode => handleCardDisplayMode(t.market, mode)}
onEnterFocus={() => setFocusMarket(t.market)}
selected={t.market === selectedMarket}
onSelect={onSelectMarket ? () => onSelectMarket(t.market) : undefined}
theme={theme}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}