From 6e86ec7ecb7eb658b085ed3d65976a926bf4984d Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 23 Jun 2026 00:12:23 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=A4=EB=A7=A4=EC=8B=9C=20=ED=98=84?= =?UTF-8?q?=EC=9E=AC=EA=B0=80=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../goldenchart/dto/BacktestSettingsDto.java | 10 +- .../entity/GcBacktestSettings.java | 8 +- .../service/BacktestSettingsService.java | 8 +- .../service/BacktestingService.java | 20 ++-- .../BarCloseStrategyEvaluationService.java | 20 ++-- .../service/LiveConditionStatusService.java | 14 +-- .../service/PaperTradingService.java | 11 +-- .../service/TradeExecutionPriceSupport.java | 96 +++++++++++++++++++ .../websocket/LiveStrategyScheduler.java | 11 ++- .../TradeExecutionPriceSupportTest.java | 52 ++++++++++ .../src/components/BacktestSettingsModal.tsx | 12 +-- .../src/components/BacktestSettingsPanel.tsx | 16 ++-- frontend/src/utils/backendApi.ts | 8 +- .../src/utils/strategyEvaluationReport.ts | 2 +- frontend/src/utils/tradeExecutionMode.ts | 4 +- packages/shared/src/api/backendApi.ts | 8 +- 16 files changed, 221 insertions(+), 79 deletions(-) create mode 100644 backend/src/main/java/com/goldenchart/service/TradeExecutionPriceSupport.java create mode 100644 backend/src/test/java/com/goldenchart/service/TradeExecutionPriceSupportTest.java diff --git a/backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java b/backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java index 45d3eed..c723040 100644 --- a/backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java +++ b/backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java @@ -32,13 +32,13 @@ public class BacktestSettingsDto { private BigDecimal slippageRate = new BigDecimal("0.00050"); // ── 진입/청산 가격 ───────────────────────────────────────────────────────── - /** CLOSE | NEXT_OPEN */ + /** CURRENT | CLOSE(레거시) | NEXT_OPEN */ @Builder.Default - private String entryPriceType = "CLOSE"; + private String entryPriceType = "CURRENT"; - /** CLOSE | NEXT_OPEN */ + /** CURRENT | CLOSE(레거시) | NEXT_OPEN */ @Builder.Default - private String exitPriceType = "CLOSE"; + private String exitPriceType = "CURRENT"; // ── 포지션 방향 ──────────────────────────────────────────────────────────── /** LONG | SHORT | BOTH */ @@ -109,7 +109,7 @@ public class BacktestSettingsDto { private String analysisMethod = "MARK_TO_MARKET"; /** - * SCAN_SIGNALS — 조건 스캔(봉 종가·DSL 충족, 차트 마커와 동일) + * SCAN_SIGNALS — 조건 스캔(현재가·DSL 충족, 차트 마커와 동일) * BACKTEST_ENGINE — 슬리피지·손절/익절·진입/청산가 등 백테스트 설정 반영 (기본) */ @Builder.Default diff --git a/backend/src/main/java/com/goldenchart/entity/GcBacktestSettings.java b/backend/src/main/java/com/goldenchart/entity/GcBacktestSettings.java index abf2df3..51b969a 100644 --- a/backend/src/main/java/com/goldenchart/entity/GcBacktestSettings.java +++ b/backend/src/main/java/com/goldenchart/entity/GcBacktestSettings.java @@ -42,15 +42,15 @@ public class GcBacktestSettings { private BigDecimal slippageRate = new BigDecimal("0.00050"); // ── 진입/청산 가격 ───────────────────────────────────────────────────────── - /** CLOSE | NEXT_OPEN */ + /** CURRENT | CLOSE(레거시) | NEXT_OPEN */ @Column(name = "entry_price_type", nullable = false, length = 20) @Builder.Default - private String entryPriceType = "CLOSE"; + private String entryPriceType = "CURRENT"; - /** CLOSE | NEXT_OPEN */ + /** CURRENT | CLOSE(레거시) | NEXT_OPEN */ @Column(name = "exit_price_type", nullable = false, length = 20) @Builder.Default - private String exitPriceType = "CLOSE"; + private String exitPriceType = "CURRENT"; // ── 포지션 방향 ──────────────────────────────────────────────────────────── /** LONG | SHORT | BOTH */ diff --git a/backend/src/main/java/com/goldenchart/service/BacktestSettingsService.java b/backend/src/main/java/com/goldenchart/service/BacktestSettingsService.java index 6bd2394..3dac6a3 100644 --- a/backend/src/main/java/com/goldenchart/service/BacktestSettingsService.java +++ b/backend/src/main/java/com/goldenchart/service/BacktestSettingsService.java @@ -33,8 +33,8 @@ public class BacktestSettingsService { entity.setCommissionType(dto.getCommissionType()); entity.setCommissionRate(dto.getCommissionRate()); entity.setSlippageRate(dto.getSlippageRate()); - entity.setEntryPriceType(dto.getEntryPriceType()); - entity.setExitPriceType(dto.getExitPriceType()); + entity.setEntryPriceType(TradeExecutionPriceSupport.normalizePriceType(dto.getEntryPriceType())); + entity.setExitPriceType(TradeExecutionPriceSupport.normalizePriceType(dto.getExitPriceType())); entity.setPositionDirection(dto.getPositionDirection()); entity.setTradeSizeType(dto.getTradeSizeType()); entity.setTradeSizeValue(dto.getTradeSizeValue()); @@ -71,8 +71,8 @@ public class BacktestSettingsService { .commissionType(e.getCommissionType()) .commissionRate(e.getCommissionRate()) .slippageRate(e.getSlippageRate()) - .entryPriceType(e.getEntryPriceType()) - .exitPriceType(e.getExitPriceType()) + .entryPriceType(TradeExecutionPriceSupport.normalizePriceType(e.getEntryPriceType())) + .exitPriceType(TradeExecutionPriceSupport.normalizePriceType(e.getExitPriceType())) .positionDirection(e.getPositionDirection()) .tradeSizeType(e.getTradeSizeType()) .tradeSizeValue(e.getTradeSizeValue()) diff --git a/backend/src/main/java/com/goldenchart/service/BacktestingService.java b/backend/src/main/java/com/goldenchart/service/BacktestingService.java index 4476d2a..f40bdd3 100644 --- a/backend/src/main/java/com/goldenchart/service/BacktestingService.java +++ b/backend/src/main/java/com/goldenchart/service/BacktestingService.java @@ -50,7 +50,7 @@ public class BacktestingService { private static final BacktestSettingsDto DEFAULT_SETTINGS = new BacktestSettingsDto(); - /** 조건 스캔 — 종가·DSL 충족 (슬리피지·손절/익절 Rule 미적용) */ + /** 조건 스캔 — 현재가·DSL 충족 (슬리피지·손절/익절 Rule 미적용) */ public static final String TRADE_EXEC_SCAN_SIGNALS = "SCAN_SIGNALS"; /** 백테스트 엔진 — 슬리피지·리스크 Rule·진입/청산가 반영 */ public static final String TRADE_EXEC_BACKTEST_ENGINE = "BACKTEST_ENGINE"; @@ -221,8 +221,12 @@ public class BacktestingService { int barCount = series.getBarCount(); int loopStart = Math.max(0, Math.min(evalStartIndex, barCount)); - final String entryPriceType = scanExec ? "CLOSE" : cfg.getEntryPriceType(); - final String exitPriceType = scanExec ? "CLOSE" : cfg.getExitPriceType(); + final String entryPriceType = scanExec + ? TradeExecutionPriceSupport.PRICE_CURRENT + : cfg.getEntryPriceType(); + final String exitPriceType = scanExec + ? TradeExecutionPriceSupport.PRICE_CURRENT + : cfg.getExitPriceType(); for (int i = loopStart; i < barCount; i++) { double closePrice = getPrice(series, req.getBars(), i, entryPriceType); @@ -730,15 +734,7 @@ public class BacktestingService { // ── 가격 결정 ───────────────────────────────────────────────────────────── private double getPrice(BarSeries series, List bars, int i, String priceType) { - if ("NEXT_OPEN".equals(priceType) && i + 1 < bars.size()) - return bars.get(i + 1).getOpen(); - if ("OPEN".equals(priceType)) - return series.getBar(i).getOpenPrice().doubleValue(); - if ("HIGH".equals(priceType)) - return series.getBar(i).getHighPrice().doubleValue(); - if ("LOW".equals(priceType)) - return series.getBar(i).getLowPrice().doubleValue(); - return series.getBar(i).getClosePrice().doubleValue(); + return TradeExecutionPriceSupport.backtestExecutionPrice(series, bars, i, priceType); } private double applySlippage(double price, BacktestSettingsDto cfg, boolean isBuy) { diff --git a/backend/src/main/java/com/goldenchart/service/BarCloseStrategyEvaluationService.java b/backend/src/main/java/com/goldenchart/service/BarCloseStrategyEvaluationService.java index c6ded57..d743e55 100644 --- a/backend/src/main/java/com/goldenchart/service/BarCloseStrategyEvaluationService.java +++ b/backend/src/main/java/com/goldenchart/service/BarCloseStrategyEvaluationService.java @@ -79,12 +79,14 @@ public class BarCloseStrategyEvaluationService { if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) continue; - publishStrategySignal(market, ct, signalBar, closeSignal, s, signalExecType); + double execPrice = TradeExecutionPriceSupport.resolveLiveExecutionPrice( + ta4jStorage, market, ct, signalBar); + publishStrategySignal(market, ct, signalBar, closeSignal, s, signalExecType, execPrice); try { tradeSignalService.save( s.getDeviceId(), s.getUserId(), market, s.getStrategyId(), null, - closeSignal, signalBar.getClosePrice().doubleValue(), + closeSignal, execPrice, candleTimeEpoch, ct, signalExecType); if (s.getUserId() != null) { String strategyName = tradeSignalService.resolveStrategyName( @@ -92,7 +94,7 @@ public class BarCloseStrategyEvaluationService { orderExecutionQueue.submitSignal( s.getUserId(), market, s.getStrategyId(), closeSignal, - signalBar.getClosePrice().doubleValue(), + execPrice, ct, strategyName, signalExecType); } } catch (Exception e) { @@ -138,8 +140,12 @@ public class BarCloseStrategyEvaluationService { } private void publishStrategySignal(String market, String candleType, Bar bar, String signal, - GcLiveStrategySettings setting, String executionType) { + GcLiveStrategySettings setting, String executionType, + double executionPrice) { long barStartEpoch = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds(); + double signalPrice = executionPrice > 0 + ? executionPrice + : TradeExecutionPriceSupport.signalBarPrice(bar); // [기존] 차트 캔들 스트림에 signal 필드 포함하여 발행 (차트 마커 렌더링용, 하위 호환) CandleBarDto dto = CandleBarDto.builder() @@ -164,15 +170,15 @@ public class BarCloseStrategyEvaluationService { .candleType(candleType) .strategyId(setting.getStrategyId()) .signalType(signal) - .price(bar.getClosePrice().doubleValue()) + .price(signalPrice) .candleTime(barStartEpoch) .executionType(executionType) .timestamp(System.currentTimeMillis()) .build(); signalEventBroker.publishToUser(setting.getUserId(), setting.getDeviceId(), signalEvent); - log.info("[BarCloseEval] bar-close signal={} market={} candleType={} strategyId={} userId={}", - signal, market, candleType, setting.getStrategyId(), setting.getUserId()); + log.info("[BarCloseEval] bar-close signal={} market={} candleType={} strategyId={} userId={} price={}", + signal, market, candleType, setting.getStrategyId(), setting.getUserId(), signalPrice); } private void trimEvaluatedKeys() { diff --git a/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java b/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java index e141f24..022234f 100644 --- a/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java +++ b/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java @@ -816,7 +816,7 @@ public class LiveConditionStatusService { boolean inPosition = false; for (int i = startIdx; i <= primarySeries.getEndIndex(); i++) { long barTimeSec = barOpenEpochSec(primarySeries, i); - double close = primarySeries.getBar(i).getClosePrice().doubleValue(); + double execPrice = TradeExecutionPriceSupport.signalBarPrice(primarySeries, i); if (signalOnly) { if (entryRule.isSatisfied(i, null)) { @@ -824,7 +824,7 @@ public class LiveConditionStatusService { signals.add(BacktestResponse.Signal.builder() .time(barTimeSec) .type("BUY") - .price(close) + .price(execPrice) .barIndex(i) .quantity(0) .build()); @@ -834,7 +834,7 @@ public class LiveConditionStatusService { signals.add(BacktestResponse.Signal.builder() .time(barTimeSec) .type("SELL") - .price(close) + .price(execPrice) .barIndex(i) .quantity(0) .build()); @@ -848,11 +848,11 @@ public class LiveConditionStatusService { signals.add(BacktestResponse.Signal.builder() .time(barTimeSec) .type("BUY") - .price(close) + .price(execPrice) .barIndex(i) .quantity(0) .build()); - record.enter(i, primarySeries.numFactory().numOf(close), + record.enter(i, primarySeries.numFactory().numOf(execPrice), primarySeries.numFactory().numOf(1)); inPosition = true; } @@ -861,11 +861,11 @@ public class LiveConditionStatusService { signals.add(BacktestResponse.Signal.builder() .time(barTimeSec) .type("SELL") - .price(close) + .price(execPrice) .barIndex(i) .quantity(0) .build()); - record.exit(i, primarySeries.numFactory().numOf(close), + record.exit(i, primarySeries.numFactory().numOf(execPrice), primarySeries.numFactory().numOf(1)); inPosition = false; } diff --git a/backend/src/main/java/com/goldenchart/service/PaperTradingService.java b/backend/src/main/java/com/goldenchart/service/PaperTradingService.java index 7a5ceb5..64f1b0e 100644 --- a/backend/src/main/java/com/goldenchart/service/PaperTradingService.java +++ b/backend/src/main/java/com/goldenchart/service/PaperTradingService.java @@ -492,16 +492,7 @@ public class PaperTradingService { } public double resolveMarkPrice(String symbol) { - try { - if (!ta4jStorage.exists(symbol, "1m")) return 0; - BarSeries series = ta4jStorage.getOrCreate(symbol, "1m"); - if (!series.isEmpty()) { - Bar last = series.getLastBar(); - return last.getClosePrice().doubleValue(); - } - } catch (Exception ignored) { - } - return 0; + return TradeExecutionPriceSupport.resolveLiveExecutionPrice(ta4jStorage, symbol, "1m", null); } public PaperSummaryDto resetAccount(Long userId) { diff --git a/backend/src/main/java/com/goldenchart/service/TradeExecutionPriceSupport.java b/backend/src/main/java/com/goldenchart/service/TradeExecutionPriceSupport.java new file mode 100644 index 0000000..f84db83 --- /dev/null +++ b/backend/src/main/java/com/goldenchart/service/TradeExecutionPriceSupport.java @@ -0,0 +1,96 @@ +package com.goldenchart.service; + +import com.goldenchart.dto.OhlcvBar; +import com.goldenchart.storage.Ta4jStorage; +import org.ta4j.core.Bar; +import org.ta4j.core.BarSeries; + +import java.util.List; + +/** + * 매매 시그널 체결가 — 종가·시장가 대신 현재가(해당 시점 최신 체결가) 기준. + * + *

히스토리 백테스트: 시그널 봉의 종가 = 그 시점의 현재가. + *

실시간: ta4j 시리즈 최신 봉 종가(틱 갱신) 우선. + */ +public final class TradeExecutionPriceSupport { + + public static final String PRICE_CURRENT = "CURRENT"; + /** 레거시 — CURRENT 와 동일 취급 */ + public static final String PRICE_CLOSE = "CLOSE"; + public static final String PRICE_NEXT_OPEN = "NEXT_OPEN"; + + private TradeExecutionPriceSupport() { + } + + /** CLOSE·MARKET·blank → CURRENT */ + public static String normalizePriceType(String priceType) { + if (priceType == null || priceType.isBlank()) return PRICE_CURRENT; + return switch (priceType.toUpperCase()) { + case PRICE_CLOSE, "MARKET" -> PRICE_CURRENT; + default -> priceType.toUpperCase(); + }; + } + + /** + * 백테스트·조건 스캔 — 시그널 발생 봉의 체결가. + */ + public static double signalBarPrice(BarSeries series, int barIndex) { + if (series == null || barIndex < series.getBeginIndex() || barIndex > series.getEndIndex()) { + return 0.0; + } + return series.getBar(barIndex).getClosePrice().doubleValue(); + } + + public static double signalBarPrice(Bar bar) { + if (bar == null) return 0.0; + return bar.getClosePrice().doubleValue(); + } + + /** + * 실시간 체결가 — 차트 분봉·1m 시리즈 최신 틱가, 없으면 fallback 봉 종가. + */ + public static double resolveLiveExecutionPrice(Ta4jStorage storage, String market, + String candleType, Bar fallbackBar) { + double fromSeries = lastSeriesClosePrice(storage, market, candleType); + if (fromSeries > 0) return fromSeries; + fromSeries = lastSeriesClosePrice(storage, market, "1m"); + if (fromSeries > 0) return fromSeries; + return signalBarPrice(fallbackBar); + } + + /** + * 백테스트 진입/청산가 — entryPriceType / exitPriceType. + */ + public static double backtestExecutionPrice(BarSeries series, List bars, + int barIndex, String priceType) { + String normalized = normalizePriceType(priceType); + if (PRICE_NEXT_OPEN.equals(normalized) && bars != null && barIndex + 1 < bars.size()) { + return bars.get(barIndex + 1).getOpen(); + } + if ("OPEN".equals(normalized)) { + return series.getBar(barIndex).getOpenPrice().doubleValue(); + } + if ("HIGH".equals(normalized)) { + return series.getBar(barIndex).getHighPrice().doubleValue(); + } + if ("LOW".equals(normalized)) { + return series.getBar(barIndex).getLowPrice().doubleValue(); + } + // CURRENT (기본) — 시그널 봉 현재가 = 종가 + return signalBarPrice(series, barIndex); + } + + private static double lastSeriesClosePrice(Ta4jStorage storage, String market, String candleType) { + if (storage == null || market == null || candleType == null) return 0.0; + try { + if (!storage.exists(market, candleType)) return 0.0; + BarSeries series = storage.getOrCreate(market, candleType); + if (series.isEmpty()) return 0.0; + double p = series.getLastBar().getClosePrice().doubleValue(); + return p > 0 ? p : 0.0; + } catch (Exception ignored) { + return 0.0; + } + } +} diff --git a/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java b/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java index b771f3b..29a98b7 100644 --- a/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java +++ b/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java @@ -6,6 +6,7 @@ import com.goldenchart.entity.GcLiveStrategySettings; import com.goldenchart.repository.GcLiveStrategySettingsRepository; import com.goldenchart.service.LiveStrategyEvaluator; import com.goldenchart.service.StrategyConditionTimeframeService; +import com.goldenchart.service.TradeExecutionPriceSupport; import com.goldenchart.service.TradeSignalService; import com.goldenchart.trading.pipeline.OrderExecutionQueue; import com.goldenchart.storage.Ta4jStorage; @@ -74,9 +75,9 @@ public class LiveStrategyScheduler { String signal = evaluator.evaluateSettingRealtimeTick(s, market, candleType); if ("BUY".equals(signal) || "SELL".equals(signal)) { - // 현재 진행 중인 캔들 정보 조회 org.ta4j.core.Bar lastBar = series.getLastBar(); - // Ta4j 0.22: getEndTime() → Instant; 시작 시간 = endTime - duration + double execPrice = TradeExecutionPriceSupport.resolveLiveExecutionPrice( + ta4jStorage, market, candleType, lastBar); long barStartEpoch = lastBar.getEndTime().getEpochSecond() - lastBar.getTimePeriod().getSeconds(); @@ -103,7 +104,7 @@ public class LiveStrategyScheduler { .candleType(candleType) .strategyId(s.getStrategyId()) .signalType(signal) - .price(lastBar.getClosePrice().doubleValue()) + .price(execPrice) .candleTime(barStartEpoch) .executionType("REALTIME_TICK") .timestamp(System.currentTimeMillis()) @@ -118,7 +119,7 @@ public class LiveStrategyScheduler { s.getDeviceId(), s.getUserId(), market, s.getStrategyId(), null, signal, - lastBar.getClosePrice().doubleValue(), + execPrice, barStartEpoch, candleType, "REALTIME_TICK" ); if (s.getUserId() != null) { @@ -127,7 +128,7 @@ public class LiveStrategyScheduler { orderExecutionQueue.submitSignal( s.getUserId(), market, s.getStrategyId(), signal, - lastBar.getClosePrice().doubleValue(), + execPrice, candleType, strategyName, "REALTIME_TICK"); } } catch (Exception ex) { diff --git a/backend/src/test/java/com/goldenchart/service/TradeExecutionPriceSupportTest.java b/backend/src/test/java/com/goldenchart/service/TradeExecutionPriceSupportTest.java new file mode 100644 index 0000000..7ffd6ce --- /dev/null +++ b/backend/src/test/java/com/goldenchart/service/TradeExecutionPriceSupportTest.java @@ -0,0 +1,52 @@ +package com.goldenchart.service; + +import com.goldenchart.dto.OhlcvBar; +import org.junit.jupiter.api.Test; +import org.ta4j.core.BarSeries; +import org.ta4j.core.BaseBarSeriesBuilder; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TradeExecutionPriceSupportTest { + + @Test + void normalizePriceType_mapsCloseAndMarketToCurrent() { + assertEquals("CURRENT", TradeExecutionPriceSupport.normalizePriceType("CLOSE")); + assertEquals("CURRENT", TradeExecutionPriceSupport.normalizePriceType("MARKET")); + assertEquals("NEXT_OPEN", TradeExecutionPriceSupport.normalizePriceType("next_open")); + } + + @Test + void backtestExecutionPrice_currentUsesSignalBarClose() { + BarSeries series = new BaseBarSeriesBuilder().build(); + Instant t0 = Instant.parse("2024-01-01T00:00:00Z"); + series.barBuilder() + .timePeriod(Duration.ofMinutes(1)) + .endTime(t0.plus(Duration.ofMinutes(1))) + .openPrice(100).highPrice(110).lowPrice(90).closePrice(105) + .add(); + series.barBuilder() + .timePeriod(Duration.ofMinutes(1)) + .endTime(t0.plus(Duration.ofMinutes(2))) + .openPrice(105).highPrice(120).lowPrice(100).closePrice(115) + .add(); + + List bars = List.of( + OhlcvBar.builder().time(t0.getEpochSecond()).open(100).high(110).low(90).close(105).build(), + OhlcvBar.builder().time(t0.plus(Duration.ofMinutes(1)).getEpochSecond()) + .open(105).high(120).low(100).close(115).build() + ); + + double current = TradeExecutionPriceSupport.backtestExecutionPrice( + series, bars, 0, "CURRENT"); + assertEquals(105.0, current, 1e-9); + + double nextOpen = TradeExecutionPriceSupport.backtestExecutionPrice( + series, bars, 0, "NEXT_OPEN"); + assertEquals(105.0, nextOpen, 1e-9); + } +} diff --git a/frontend/src/components/BacktestSettingsModal.tsx b/frontend/src/components/BacktestSettingsModal.tsx index 4fd2072..b0ae72f 100644 --- a/frontend/src/components/BacktestSettingsModal.tsx +++ b/frontend/src/components/BacktestSettingsModal.tsx @@ -18,7 +18,7 @@ const FIELD_META: Partial> = { tradeExecutionMode: { label: '매매 체결 방식', description: - '시그널·체결가 산출 방식입니다.\n• 백테스트 엔진: 슬리피지, 손절/익절, 진입/청산가 반영. 실제 수익률 분석에 가깝습니다.\n• 조건 스캔: 봉 종가·DSL 충족 기준. 차트 마커·조건 패널과 동일합니다.', + '시그널·체결가 산출 방식입니다.\n• 백테스트 엔진: 슬리피지, 손절/익절, 진입/청산가 반영. 실제 수익률 분석에 가깝습니다.\n• 조건 스캔: 현재가·DSL 충족 기준. 차트 마커·조건 패널과 동일합니다.', }, positionMode: { label: '시그널 생성 방식', @@ -53,12 +53,12 @@ const FIELD_META: Partial> = { entryPriceType: { label: '진입 가격 기준', description: - '매수 신호 발생 시 어느 가격에 진입하는지 결정합니다.\n• CLOSE(종가): 신호 발생 봉의 종가로 즉시 진입. 구현이 단순하지만 후행성(look-ahead bias) 우려가 있습니다.\n• NEXT_OPEN(다음봉 시가): 신호 다음 봉 시가에 진입. 실제 트레이딩과 가장 유사한 현실적 시뮬레이션입니다.', + '매수 신호 발생 시 어느 가격에 진입하는지 결정합니다.\n• CURRENT(현재가): 신호 발생 시점의 현재가로 즉시 진입.\n• NEXT_OPEN(다음봉 시가): 신호 다음 봉 시가에 진입. 실제 트레이딩과 가장 유사한 현실적 시뮬레이션입니다.', }, exitPriceType: { label: '청산 가격 기준', description: - '매도 신호 발생 시 어느 가격에 청산하는지 결정합니다.\n• CLOSE(종가): 신호 발생 봉의 종가로 즉시 청산합니다.\n• NEXT_OPEN(다음봉 시가): 신호 다음 봉 시가에 청산. 야간 갭 하락 등 실제 리스크를 반영합니다.', + '매도 신호 발생 시 어느 가격에 청산하는지 결정합니다.\n• CURRENT(현재가): 신호 발생 시점의 현재가로 즉시 청산합니다.\n• NEXT_OPEN(다음봉 시가): 신호 다음 봉 시가에 청산. 야간 갭 하락 등 실제 리스크를 반영합니다.', }, positionDirection: { label: '포지션 방향', @@ -151,7 +151,7 @@ const SECTIONS: SettingSection[] = [ key: 'tradeExecutionMode', type: 'select', opts: [ { value: 'BACKTEST_ENGINE', label: '백테스트 엔진 (슬리피지·리스크 반영)' }, - { value: 'SCAN_SIGNALS', label: '조건 스캔 (봉 종가·DSL 충족)' }, + { value: 'SCAN_SIGNALS', label: '조건 스캔 (현재가·DSL 충족)' }, ], }, ], @@ -203,11 +203,11 @@ const SECTIONS: SettingSection[] = [ fields: [ { key: 'entryPriceType', type: 'select', - opts: [{ value:'CLOSE', label:'종가 (Close)' }, { value:'NEXT_OPEN', label:'다음봉 시가 (Next Open)' }], + opts: [{ value:'CURRENT', label:'현재가 (Current)' }, { value:'NEXT_OPEN', label:'다음봉 시가 (Next Open)' }], }, { key: 'exitPriceType', type: 'select', - opts: [{ value:'CLOSE', label:'종가 (Close)' }, { value:'NEXT_OPEN', label:'다음봉 시가 (Next Open)' }], + opts: [{ value:'CURRENT', label:'현재가 (Current)' }, { value:'NEXT_OPEN', label:'다음봉 시가 (Next Open)' }], }, { key: 'positionDirection', type: 'select', diff --git a/frontend/src/components/BacktestSettingsPanel.tsx b/frontend/src/components/BacktestSettingsPanel.tsx index edf719b..ce1ad84 100644 --- a/frontend/src/components/BacktestSettingsPanel.tsx +++ b/frontend/src/components/BacktestSettingsPanel.tsx @@ -233,19 +233,19 @@ export const BacktestSettingsPanel: React.FC = ({ - + - + diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index 483de4a..864b116 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -1419,8 +1419,8 @@ export interface BacktestSettingsDto { commissionType: 'LINEAR' | 'ZERO'; commissionRate: number; slippageRate: number; - entryPriceType: 'CLOSE' | 'NEXT_OPEN'; - exitPriceType: 'CLOSE' | 'NEXT_OPEN'; + entryPriceType: 'CURRENT' | 'CLOSE' | 'NEXT_OPEN'; + exitPriceType: 'CURRENT' | 'CLOSE' | 'NEXT_OPEN'; positionDirection: 'LONG' | 'SHORT' | 'BOTH'; tradeSizeType: 'CAPITAL_PCT' | 'FIXED_AMOUNT'; tradeSizeValue: number; @@ -1447,8 +1447,8 @@ export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = { commissionType: 'LINEAR', commissionRate: 0.0015, slippageRate: 0.0005, - entryPriceType: 'CLOSE', - exitPriceType: 'CLOSE', + entryPriceType: 'CURRENT', + exitPriceType: 'CURRENT', positionDirection: 'LONG', tradeSizeType: 'CAPITAL_PCT', tradeSizeValue: 100, diff --git a/frontend/src/utils/strategyEvaluationReport.ts b/frontend/src/utils/strategyEvaluationReport.ts index e72c0db..038ac5c 100644 --- a/frontend/src/utils/strategyEvaluationReport.ts +++ b/frontend/src/utils/strategyEvaluationReport.ts @@ -432,7 +432,7 @@ export function buildStrategyEvaluationReportModel(opts: { '분석 기간 전체 캔들 기준으로, 최초 매수 이전 매도·최종 매도 이후 매수 시그널은 제외했습니다. ' + '매도는 보유 물량 전량 매도로 가정합니다. ' + (opts.tradeExecutionMode === 'SCAN_SIGNALS' - ? '체결가: 조건 스캔(봉 종가·DSL 충족).' + ? '체결가: 조건 스캔(현재가·DSL 충족).' : '체결가: 백테스트 설정(슬리피지·손절/익절·진입/청산가) 반영.'), }; } diff --git a/frontend/src/utils/tradeExecutionMode.ts b/frontend/src/utils/tradeExecutionMode.ts index ad0b782..b29a06f 100644 --- a/frontend/src/utils/tradeExecutionMode.ts +++ b/frontend/src/utils/tradeExecutionMode.ts @@ -14,8 +14,8 @@ export const TRADE_EXECUTION_MODE_OPTIONS: { }, { value: 'SCAN_SIGNALS', - label: '조건 스캔 (봉 종가·DSL 충족)', - desc: '차트 마커·조건 패널과 동일. 봉 종가 기준 이상적 체결.', + label: '조건 스캔 (현재가·DSL 충족)', + desc: '차트 마커·조건 패널과 동일. 시그널 시점 현재가 기준 체결.', }, ]; diff --git a/packages/shared/src/api/backendApi.ts b/packages/shared/src/api/backendApi.ts index 784017a..e794dc8 100644 --- a/packages/shared/src/api/backendApi.ts +++ b/packages/shared/src/api/backendApi.ts @@ -1130,8 +1130,8 @@ export interface BacktestSettingsDto { commissionType: 'LINEAR' | 'ZERO'; commissionRate: number; slippageRate: number; - entryPriceType: 'CLOSE' | 'NEXT_OPEN'; - exitPriceType: 'CLOSE' | 'NEXT_OPEN'; + entryPriceType: 'CURRENT' | 'CLOSE' | 'NEXT_OPEN'; + exitPriceType: 'CURRENT' | 'CLOSE' | 'NEXT_OPEN'; positionDirection: 'LONG' | 'SHORT' | 'BOTH'; tradeSizeType: 'CAPITAL_PCT' | 'FIXED_AMOUNT'; tradeSizeValue: number; @@ -1154,8 +1154,8 @@ export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = { commissionType: 'LINEAR', commissionRate: 0.0015, slippageRate: 0.0005, - entryPriceType: 'CLOSE', - exitPriceType: 'CLOSE', + entryPriceType: 'CURRENT', + exitPriceType: 'CURRENT', positionDirection: 'LONG', tradeSizeType: 'CAPITAL_PCT', tradeSizeValue: 100,