가상매매 시간봉 제거

This commit is contained in:
Macbook
2026-05-28 20:48:28 +09:00
parent 7e3644cb62
commit 15160f7d2c
45 changed files with 996 additions and 405 deletions
@@ -183,7 +183,6 @@ const TrendSearchPage: React.FC<Props> = ({
void add(row.market, {
koreanName,
englishName,
candleType: filtersRef.current.timeframe,
});
}, [add]);
+6 -57
View File
@@ -43,9 +43,7 @@ import {
type VirtualSessionConfig,
type VirtualTargetItem,
type VirtualCardViewMode,
resolveTargetCandleType,
} from '../utils/virtualTradingStorage';
import { normalizeStartCandleType } from '../utils/strategyStartNodes';
import { useAppSettings, resolveAppDefaults } from '../hooks/useAppSettings';
import { coerceFiniteNumber } from '../utils/safeFormat';
import {
@@ -179,7 +177,11 @@ const VirtualTradingPage: React.FC<Props> = ({
})),
[targets, session.globalStrategyId]);
const snapshots = useVirtualIndicatorSnapshots(targetRefs, strategies, session.running);
const { snapshots, loadingByMarket: snapshotLoadingByMarket } = useVirtualIndicatorSnapshots(
targetRefs,
strategies,
session.running,
);
const { statusByMarket: liveStatusByMarket, lastTickAtByMarket } = useVirtualTargetLiveStatus(
targetRefs,
session.running,
@@ -341,59 +343,6 @@ const VirtualTradingPage: React.FC<Props> = ({
}
}, [session]);
const handleTargetCandleTypeChange = useCallback(async (market: string, candleType: string) => {
const normalized = normalizeStartCandleType(candleType);
setTargets(prev => prev.map(t =>
t.market === market ? { ...t, candleType: normalized } : t,
));
if (!session.running) return;
const target = targets.find(t => t.market === market);
const strategyId = target
? resolveVirtualTargetStrategyId(target, session.globalStrategyId)
: session.globalStrategyId;
if (!strategyId) return;
try {
await saveLiveStrategySettings({
market,
strategyId,
isLiveCheck: true,
executionType: session.executionType,
positionMode: session.positionMode,
skipWatchlistSync: true,
skipGlobalTemplate: true,
});
if (strategyId) {
await pinStrategyEvaluationTimeframes(market, strategyId);
}
} catch {
window.alert('종목 평가 분봉 변경 저장에 실패했습니다.');
}
}, [session, targets]);
useEffect(() => {
let cancelled = false;
const missing = targets.filter(t => !t.candleType);
if (missing.length === 0) return;
void Promise.all(
missing.map(async t => {
try {
const s = await loadLiveStrategySettings(t.market);
return { market: t.market, candleType: s.candleType };
} catch {
return { market: t.market, candleType: undefined };
}
}),
).then(rows => {
if (cancelled) return;
setTargets(prev => prev.map(t => {
const row = rows.find(r => r.market === t.market);
if (t.candleType || !row?.candleType) return t;
return { ...t, candleType: normalizeStartCandleType(row.candleType) };
}));
});
return () => { cancelled = true; };
}, [targets.map(t => `${t.market}:${t.candleType ?? ''}`).join('|')]);
const resyncRunningSession = useCallback(async (nextSession: VirtualSessionConfig) => {
if (!session.running || targets.length === 0) return;
try {
@@ -483,9 +432,9 @@ const VirtualTradingPage: React.FC<Props> = ({
targets={targets}
strategies={strategies}
snapshots={snapshots}
snapshotLoadingByMarket={snapshotLoadingByMarket}
session={session}
onTargetStrategyChange={(market, strategyId) => void handleTargetStrategyChange(market, strategyId)}
onTargetCandleTypeChange={(market, ct) => void handleTargetCandleTypeChange(market, ct)}
liveStatusByMarket={mergedLiveStatus}
lastTickAtByMarket={lastTickAtByMarket}
selectedMarket={selectedMarket}
@@ -5,8 +5,7 @@ import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSn
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
import { buildConditionMetrics } from '../../utils/virtualSignalMetrics';
import { candleTypeToTimeframe } from '../../utils/strategyToChartIndicators';
import { normalizeStartCandleType } from '../../utils/strategyStartNodes';
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
import type { Timeframe } from '../../types';
import VirtualLiveBadge from './VirtualLiveBadge';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
@@ -27,9 +26,8 @@ interface Props {
strategyId: number | null;
globalStrategyId: number | null;
onStrategyChange?: (strategyId: number | null) => void;
candleType: string;
onCandleTypeChange?: (candleType: string) => void;
snapshot: VirtualIndicatorSnapshot | undefined;
signalLoading?: boolean;
running: boolean;
liveStatus?: VirtualLiveStatus;
lastTickAt?: number;
@@ -54,9 +52,8 @@ const VirtualTargetCard: React.FC<Props> = ({
strategyId,
globalStrategyId,
onStrategyChange,
candleType,
onCandleTypeChange,
snapshot,
signalLoading = false,
running,
liveStatus = 'idle',
lastTickAt,
@@ -83,8 +80,10 @@ const VirtualTargetCard: React.FC<Props> = ({
const isDetail = viewMode === 'detail';
const isChart = displayMode === 'chart';
const evalCandleType = normalizeStartCandleType(candleType);
const chartTimeframe = candleTypeToTimeframe(evalCandleType) as Timeframe;
const chartTimeframe = useMemo(
() => resolveStrategyPrimaryTimeframe(strategy) as Timeframe,
[strategy],
);
const receiveSignal = useMemo(() => {
if (!running) return null;
@@ -184,6 +183,7 @@ const VirtualTargetCard: React.FC<Props> = ({
snapshot={snapshot}
viewMode={viewMode}
receiving={highlightReceiving}
loading={signalLoading}
/>
)}
@@ -192,9 +192,7 @@ const VirtualTargetCard: React.FC<Props> = ({
strategies={strategies}
strategyId={strategyId}
globalStrategyId={globalStrategyId}
candleType={evalCandleType}
onStrategyChange={onStrategyChange}
onCandleTypeChange={onCandleTypeChange}
/>
</div>
);
@@ -34,7 +34,7 @@ interface Props {
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
/** 전체보기 분할 pane — 캔버스 높이 100% */
fillHeight?: boolean;
/** 카드 푸터 평가 분봉과 동기화 */
/** 미지정 시 전략 DSL 대표 분봉 사용 */
chartTimeframe?: Timeframe;
}
@@ -2,7 +2,6 @@ import React from 'react';
import type { StrategyDto } from '../../utils/backendApi';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import { formatUpdatedTime } from '../../utils/virtualSignalMetrics';
import { STRATEGY_CANDLE_TYPE_OPTIONS } from '../../utils/strategyStartNodes';
import {
defaultStrategyOptionLabel,
parseTargetStrategySelectValue,
@@ -14,20 +13,16 @@ interface Props {
strategies: StrategyDto[];
strategyId: number | null;
globalStrategyId: number | null;
candleType: string;
onStrategyChange?: (strategyId: number | null) => void;
onCandleTypeChange?: (candleType: string) => void;
}
/** 카드 하단 — 갱신 시각(좌) · 전략·시간봉 드롭다운(우) */
/** 카드 하단 — 갱신 시각(좌) · 전략 드롭다운(우) */
const VirtualTargetCardFoot: React.FC<Props> = ({
snapshot,
strategies,
strategyId,
globalStrategyId,
candleType,
onStrategyChange,
onCandleTypeChange,
}) => (
<div className="vtd-card-foot">
<span className="vtd-card-updated"> {formatUpdatedTime(snapshot?.updatedAt)}</span>
@@ -49,18 +44,6 @@ const VirtualTargetCardFoot: React.FC<Props> = ({
))}
</select>
</label>
<label className="vtd-card-foot-field">
<select
className="vtd-card-foot-select vtd-card-foot-select--tf"
aria-label="시간봉"
value={candleType}
onChange={e => onCandleTypeChange?.(e.target.value)}
>
{STRATEGY_CANDLE_TYPE_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</label>
</div>
</div>
);
@@ -13,8 +13,7 @@ import VirtualTargetQuote from './VirtualTargetQuote';
import VirtualTargetCardChart from './VirtualTargetCardChart';
import VirtualTargetSignalPanel from './VirtualTargetSignalPanel';
import VirtualTargetCardFoot from './VirtualTargetCardFoot';
import { candleTypeToTimeframe } from '../../utils/strategyToChartIndicators';
import { normalizeStartCandleType } from '../../utils/strategyStartNodes';
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
import type { Timeframe } from '../../types';
interface Props {
@@ -24,9 +23,8 @@ interface Props {
strategyId: number | null;
globalStrategyId: number | null;
onStrategyChange?: (strategyId: number | null) => void;
candleType: string;
onCandleTypeChange?: (candleType: string) => void;
snapshot: VirtualIndicatorSnapshot | undefined;
signalLoading?: boolean;
running: boolean;
liveStatus?: VirtualLiveStatus;
lastTickAt?: number;
@@ -48,9 +46,8 @@ const VirtualTargetFocusView: React.FC<Props> = ({
strategyId,
globalStrategyId,
onStrategyChange,
candleType,
onCandleTypeChange,
snapshot,
signalLoading = false,
running,
liveStatus = 'idle',
lastTickAt,
@@ -72,8 +69,10 @@ const VirtualTargetFocusView: React.FC<Props> = ({
}, [running, lastTickAt, snapshot?.updatedAt]);
const receiving = useLiveReceiveFlash(receiveSignal, running);
const highlightReceiving = chartLiveReceiveHighlight && receiving;
const evalCandleType = normalizeStartCandleType(candleType);
const chartTimeframe = candleTypeToTimeframe(evalCandleType) as Timeframe;
const chartTimeframe = useMemo(
() => resolveStrategyPrimaryTimeframe(strategy) as Timeframe,
[strategy],
);
return (
<div className={`vtd-focus-wrap${highlightReceiving ? ' vtd-focus-wrap--receiving' : ''}`}>
@@ -129,6 +128,7 @@ const VirtualTargetFocusView: React.FC<Props> = ({
snapshot={snapshot}
viewMode={viewMode}
receiving={highlightReceiving}
loading={signalLoading}
/>
</div>
</section>
@@ -139,9 +139,7 @@ const VirtualTargetFocusView: React.FC<Props> = ({
strategies={strategies}
strategyId={strategyId}
globalStrategyId={globalStrategyId}
candleType={evalCandleType}
onStrategyChange={onStrategyChange}
onCandleTypeChange={onCandleTypeChange}
/>
</div>
);
@@ -8,7 +8,6 @@ import {
type VirtualTargetItem,
type VirtualCardViewMode,
type VirtualSessionConfig,
resolveTargetCandleType,
} from '../../utils/virtualTradingStorage';
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
@@ -19,9 +18,9 @@ interface Props {
targets: VirtualTargetItem[];
strategies: StrategyDto[];
snapshots: Record<string, VirtualIndicatorSnapshot>;
snapshotLoadingByMarket?: Record<string, boolean>;
session: Pick<VirtualSessionConfig, 'globalStrategyId' | 'executionType' | 'positionMode' | 'running'>;
onTargetStrategyChange: (market: string, strategyId: number | null) => void;
onTargetCandleTypeChange: (market: string, candleType: string) => void;
liveStatusByMarket?: Record<string, VirtualLiveStatus>;
lastTickAtByMarket?: Record<string, number>;
selectedMarket?: string;
@@ -38,9 +37,8 @@ interface Props {
}
const VirtualTargetGrid: React.FC<Props> = ({
targets, strategies, snapshots, session,
targets, strategies, snapshots, snapshotLoadingByMarket = {}, session,
onTargetStrategyChange,
onTargetCandleTypeChange,
liveStatusByMarket = {}, lastTickAtByMarket = {}, selectedMarket,
onSelectMarket,
theme = 'dark',
@@ -119,9 +117,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
strategyId={focusTarget.strategyId}
globalStrategyId={session.globalStrategyId}
onStrategyChange={id => onTargetStrategyChange(focusTarget.market, id)}
candleType={resolveTargetCandleType(focusTarget, snapshots[focusTarget.market]?.timeframe)}
onCandleTypeChange={ct => onTargetCandleTypeChange(focusTarget.market, ct)}
snapshot={snapshots[focusTarget.market]}
signalLoading={snapshotLoadingByMarket[focusTarget.market]}
running={session.running}
liveStatus={liveStatusByMarket[focusTarget.market] ?? (session.running ? 'connecting' : 'idle')}
lastTickAt={lastTickAtByMarket[focusTarget.market]}
@@ -147,9 +144,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
strategyId={t.strategyId}
globalStrategyId={session.globalStrategyId}
onStrategyChange={id => onTargetStrategyChange(t.market, id)}
candleType={resolveTargetCandleType(t, snapshots[t.market]?.timeframe)}
onCandleTypeChange={ct => onTargetCandleTypeChange(t.market, ct)}
snapshot={snapshots[t.market]}
signalLoading={snapshotLoadingByMarket[t.market]}
running={session.running}
liveStatus={liveStatusByMarket[t.market] ?? (session.running ? 'connecting' : 'idle')}
lastTickAt={lastTickAtByMarket[t.market]}
@@ -15,6 +15,8 @@ interface Props {
snapshot: VirtualIndicatorSnapshot | undefined;
viewMode?: VirtualCardViewMode;
receiving?: boolean;
/** 업비트 캔들 warm-up · live-conditions 수집 중 */
loading?: boolean;
className?: string;
}
@@ -22,6 +24,7 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
snapshot,
viewMode = 'summary',
receiving = false,
loading = false,
className = '',
}) => {
const rows = snapshot?.rows ?? [];
@@ -36,8 +39,10 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
if (rows.length === 0) {
return (
<p className={`vtd-muted vtd-card-empty${className ? ` ${className}` : ''}`}>
·
<p className={`vtd-muted vtd-card-empty${loading ? ' vtd-card-empty--loading' : ''}${className ? ` ${className}` : ''}`}>
{loading
? '지표 데이터 수집 중… (업비트 캔들 · 실시간 연결)'
: '전략 조건·지표 데이터 없음'}
</p>
);
}