From 31021695411869f7a7fee9ff6380f7f8a5285c99 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 25 May 2026 14:05:13 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=80=EC=83=81=ED=88=AC=EC=9E=90=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=AA=A9=EB=A1=9D=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/BacktestingService.java | 1 + .../goldenchart/service/IndicatorService.java | 1 + .../service/LiveStrategyTimeframeService.java | 2 +- .../com/goldenchart/storage/Ta4jStorage.java | 4 ++- .../pipeline/CandleGapBackfillService.java | 2 +- .../com/goldenchart/websocket/BarBuilder.java | 10 +++--- frontend/src/components/BottomBar.tsx | 2 ++ frontend/src/components/ChartSlot.tsx | 2 +- .../src/components/IndicatorSettingsForm.tsx | 1 + frontend/src/components/Toolbar.tsx | 2 +- .../src/components/VirtualTradingPage.tsx | 23 ++++++++++--- .../src/components/paper/PaperMiniChart.tsx | 2 +- .../components/virtual/VirtualTargetCard.tsx | 22 +++++++++++-- .../components/virtual/VirtualTargetGrid.tsx | 8 ++++- .../src/styles/virtualTradingDashboard.css | 33 +++++++++++++++++++ frontend/src/types/index.ts | 2 +- frontend/src/utils/chartCandleType.ts | 2 +- frontend/src/utils/dataGenerator.ts | 4 +-- .../src/utils/strategyDescriptionNarrative.ts | 1 + frontend/src/utils/strategyStartNodes.ts | 2 +- .../src/utils/strategyToChartIndicators.ts | 4 +-- frontend/src/utils/tradeSignalDisplay.ts | 1 + frontend/src/utils/upbitApi.ts | 3 +- frontend/src/utils/virtualSignalMetrics.ts | 23 +++++++++++++ 24 files changed, 130 insertions(+), 27 deletions(-) diff --git a/backend/src/main/java/com/goldenchart/service/BacktestingService.java b/backend/src/main/java/com/goldenchart/service/BacktestingService.java index 6bca788..8282de3 100644 --- a/backend/src/main/java/com/goldenchart/service/BacktestingService.java +++ b/backend/src/main/java/com/goldenchart/service/BacktestingService.java @@ -535,6 +535,7 @@ public class BacktestingService { case "1m" -> Duration.ofMinutes(1); case "3m" -> Duration.ofMinutes(3); case "5m" -> Duration.ofMinutes(5); + case "10m" -> Duration.ofMinutes(10); case "15m" -> Duration.ofMinutes(15); case "30m" -> Duration.ofMinutes(30); case "1h" -> Duration.ofHours(1); diff --git a/backend/src/main/java/com/goldenchart/service/IndicatorService.java b/backend/src/main/java/com/goldenchart/service/IndicatorService.java index 4be9902..dbe91f6 100644 --- a/backend/src/main/java/com/goldenchart/service/IndicatorService.java +++ b/backend/src/main/java/com/goldenchart/service/IndicatorService.java @@ -163,6 +163,7 @@ public class IndicatorService { case "1m" -> Duration.ofMinutes(1); case "3m" -> Duration.ofMinutes(3); case "5m" -> Duration.ofMinutes(5); + case "10m" -> Duration.ofMinutes(10); case "15m" -> Duration.ofMinutes(15); case "30m" -> Duration.ofMinutes(30); case "1h" -> Duration.ofHours(1); diff --git a/backend/src/main/java/com/goldenchart/service/LiveStrategyTimeframeService.java b/backend/src/main/java/com/goldenchart/service/LiveStrategyTimeframeService.java index cdd0f15..cf26bca 100644 --- a/backend/src/main/java/com/goldenchart/service/LiveStrategyTimeframeService.java +++ b/backend/src/main/java/com/goldenchart/service/LiveStrategyTimeframeService.java @@ -66,7 +66,7 @@ public class LiveStrategyTimeframeService { private static String mapChartTfToCandleType(String chartTf) { return switch (chartTf) { - case "1m", "3m", "5m", "15m", "30m" -> chartTf; + case "1m", "3m", "5m", "10m", "15m", "30m" -> chartTf; case "1h", "4h" -> chartTf; case "1D" -> "1d"; case "1W", "1M" -> "1d"; diff --git a/backend/src/main/java/com/goldenchart/storage/Ta4jStorage.java b/backend/src/main/java/com/goldenchart/storage/Ta4jStorage.java index c6a56a6..3174911 100644 --- a/backend/src/main/java/com/goldenchart/storage/Ta4jStorage.java +++ b/backend/src/main/java/com/goldenchart/storage/Ta4jStorage.java @@ -25,7 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; * * 구조: ConcurrentHashMap> * - market : "KRW-BTC", "KRW-ETH" 등 업비트 종목 코드 - * - candleType: "1m", "3m", "5m", "15m", "30m", "1h", "4h", "1d" + * - candleType: "1m", "3m", "5m", "10m", "15m", "30m", "1h", "4h", "1d" * * 슬라이딩 윈도우: 모든 BarSeries 에 setMaximumBarCount(300) 적용. * 301번째 캔들이 들어오면 가장 오래된 캔들이 자동 소멸. @@ -51,6 +51,7 @@ public class Ta4jStorage { "1m", "minutes/1", "3m", "minutes/3", "5m", "minutes/5", + "10m", "minutes/10", "15m", "minutes/15", "30m", "minutes/30", "1h", "minutes/60", @@ -231,6 +232,7 @@ public class Ta4jStorage { case "1m" -> Duration.ofMinutes(1); case "3m" -> Duration.ofMinutes(3); case "5m" -> Duration.ofMinutes(5); + case "10m" -> Duration.ofMinutes(10); case "15m" -> Duration.ofMinutes(15); case "30m" -> Duration.ofMinutes(30); case "1h" -> Duration.ofHours(1); diff --git a/backend/src/main/java/com/goldenchart/trading/pipeline/CandleGapBackfillService.java b/backend/src/main/java/com/goldenchart/trading/pipeline/CandleGapBackfillService.java index 3ad4f43..67e7e68 100644 --- a/backend/src/main/java/com/goldenchart/trading/pipeline/CandleGapBackfillService.java +++ b/backend/src/main/java/com/goldenchart/trading/pipeline/CandleGapBackfillService.java @@ -48,7 +48,7 @@ public class CandleGapBackfillService { @Value("${upbit.api.request-delay-ms:110}") private long requestDelayMs; - private static final String[] BACKFILL_TYPES = {"1m", "3m", "5m", "15m", "30m", "1h", "4h", "1d"}; + private static final String[] BACKFILL_TYPES = {"1m", "3m", "5m", "10m", "15m", "30m", "1h", "4h", "1d"}; private volatile long lastRunAtMs; private final AtomicInteger lastRunMarkets = new AtomicInteger(); diff --git a/backend/src/main/java/com/goldenchart/websocket/BarBuilder.java b/backend/src/main/java/com/goldenchart/websocket/BarBuilder.java index 5aa61f9..0afd417 100644 --- a/backend/src/main/java/com/goldenchart/websocket/BarBuilder.java +++ b/backend/src/main/java/com/goldenchart/websocket/BarBuilder.java @@ -32,9 +32,9 @@ import java.util.concurrent.ConcurrentHashMap; * 명세서 3.2 준수: * - 동일 분(minute) 내 틱 → series.addPrice(price, volume) 로 현재 Bar 갱신 * - 분이 바뀌는 틱 → 이전 Bar 를 확정하고 series.addBar(newBar) 로 새 캔들 열기 - * - 1분봉 확정 시 상위 타임프레임(3m, 5m, 15m, 30m, 1h, 4h, 1d)으로 집계 전파 + * - 1분봉 확정 시 상위 타임프레임(3m, 5m, 10m, 15m, 30m, 1h, 4h, 1d)으로 집계 전파 * - * 지원 캔들 타입: 1m, 3m, 5m, 15m, 30m, 1h, 4h, 1d + * 지원 캔들 타입: 1m, 3m, 5m, 10m, 15m, 30m, 1h, 4h, 1d */ @Component @RequiredArgsConstructor @@ -49,9 +49,9 @@ public class BarBuilder { private final GcLiveStrategySettingsRepository liveSettingsRepo; private final StrategyConditionTimeframeService conditionTimeframes; - /** 상위 집계 타임프레임 (분 단위 집계 주기: 3, 5, 15, 30, 60, 240, 1440) */ - private static final int[] UPPER_MINUTES = {3, 5, 15, 30, 60, 240, 1440}; - private static final String[] UPPER_TYPES = {"3m", "5m", "15m", "30m", "1h", "4h", "1d"}; + /** 상위 집계 타임프레임 (분 단위 집계 주기: 3, 5, 10, 15, 30, 60, 240, 1440) */ + private static final int[] UPPER_MINUTES = {3, 5, 10, 15, 30, 60, 240, 1440}; + private static final String[] UPPER_TYPES = {"3m", "5m", "10m", "15m", "30m", "1h", "4h", "1d"}; /** 진행 중인 1분봉 상태 (market → PartialBar) */ private final ConcurrentHashMap partialBars = new ConcurrentHashMap<>(); diff --git a/frontend/src/components/BottomBar.tsx b/frontend/src/components/BottomBar.tsx index 6f84bf5..7eda005 100644 --- a/frontend/src/components/BottomBar.tsx +++ b/frontend/src/components/BottomBar.tsx @@ -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' }, diff --git a/frontend/src/components/ChartSlot.tsx b/frontend/src/components/ChartSlot.tsx index e2260fc..a0bbfe1 100644 --- a/frontend/src/components/ChartSlot.tsx +++ b/frontend/src/components/ChartSlot.tsx @@ -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 }) => { diff --git a/frontend/src/components/IndicatorSettingsForm.tsx b/frontend/src/components/IndicatorSettingsForm.tsx index de2e977..a9e429b 100644 --- a/frontend/src/components/IndicatorSettingsForm.tsx +++ b/frontend/src/components/IndicatorSettingsForm.tsx @@ -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시간' }, diff --git a/frontend/src/components/Toolbar.tsx b/frontend/src/components/Toolbar.tsx index a1d56d6..e4ddfee 100644 --- a/frontend/src/components/Toolbar.tsx +++ b/frontend/src/components/Toolbar.tsx @@ -286,7 +286,7 @@ const ChartIcon: Record = { baseline: , }; -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: '바' }, diff --git a/frontend/src/components/VirtualTradingPage.tsx b/frontend/src/components/VirtualTradingPage.tsx index 852405a..30f4dba 100644 --- a/frontend/src/components/VirtualTradingPage.tsx +++ b/frontend/src/components/VirtualTradingPage.tsx @@ -110,12 +110,23 @@ const VirtualTradingPage: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ tradePrice={tradePrice} availableCoinQty={posQty} fillRequest={fillSell} - onMarketSelect={setSelectedMarket} + onMarketSelect={handleSelectMarket} showSymbolField={false} paperTradingEnabled={paperTradingEnabled} paperAutoTradeEnabled={paperAutoTradeEnabled} diff --git a/frontend/src/components/paper/PaperMiniChart.tsx b/frontend/src/components/paper/PaperMiniChart.tsx index 7434d84..5cba84c 100644 --- a/frontend/src/components/paper/PaperMiniChart.tsx +++ b/frontend/src/components/paper/PaperMiniChart.tsx @@ -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; diff --git a/frontend/src/components/virtual/VirtualTargetCard.tsx b/frontend/src/components/virtual/VirtualTargetCard.tsx index d623509..84454b1 100644 --- a/frontend/src/components/virtual/VirtualTargetCard.tsx +++ b/frontend/src/components/virtual/VirtualTargetCard.tsx @@ -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 = ({ displayMode = 'signal', onDisplayModeChange, onEnterFocus, + selected = false, + onSelect, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', chartSeriesPriceLabels = true, @@ -59,6 +63,7 @@ const VirtualTargetCard: React.FC = ({ 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 = ({ 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} >
@@ -122,7 +140,7 @@ const VirtualTargetCard: React.FC = ({ )} -
+
e.stopPropagation()}>