가상투자 화면 목록 선택 동작
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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시간' },
|
||||
|
||||
@@ -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: '바' },
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user