From 8d019ddd26a9ce03ed1754a41877f09c5a640983 Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 11 Jun 2026 17:22:26 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=ED=85=8C=EC=8A=A4=ED=8C=85=20SIGNAL?= =?UTF-8?q?=5FONLY=20=EC=88=98=EC=A0=95=20=E2=80=94=20=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=EC=85=98=20=EB=AC=B4=EA=B4=80=20=EC=A0=84=EC=B2=B4=20=EC=8B=9C?= =?UTF-8?q?=EA=B7=B8=EB=84=90=20=EC=83=9D=EC=84=B1=20+=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BacktestingService: SIGNAL_ONLY 모드에서 inPosition 포지션 락 제거, entryRule/exitRule 충족 시마다 BUY/SELL 시그널 무조건 생성 - BacktestSettingsDto: positionMode 기본값 LONG_ONLY → SIGNAL_ONLY - backendApi.ts: DEFAULT_BACKTEST_SETTINGS positionMode 기본값 변경 - BacktestSettingsModal: positionMode UI 항목(시그널 생성 방식) 최상단에 추가 Co-authored-by: Cursor --- .../goldenchart/dto/BacktestSettingsDto.java | 2 +- .../service/BacktestingService.java | 58 ++++--------------- .../src/components/BacktestSettingsModal.tsx | 17 ++++++ frontend/src/utils/backendApi.ts | 2 +- 4 files changed, 29 insertions(+), 50 deletions(-) diff --git a/backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java b/backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java index 0eac027..a83a1f1 100644 --- a/backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java +++ b/backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java @@ -90,7 +90,7 @@ public class BacktestSettingsDto { // ── 포지션 종속성 모드 ───────────────────────────────────────────────────── /** "LONG_ONLY" | "SIGNAL_ONLY" — 매도 시그널 포지션 종속성 제어 */ @Builder.Default - private String positionMode = "LONG_ONLY"; + private String positionMode = "SIGNAL_ONLY"; // ── 분할 청산 ────────────────────────────────────────────────────────────── @Builder.Default diff --git a/backend/src/main/java/com/goldenchart/service/BacktestingService.java b/backend/src/main/java/com/goldenchart/service/BacktestingService.java index dacac76..d383929 100644 --- a/backend/src/main/java/com/goldenchart/service/BacktestingService.java +++ b/backend/src/main/java/com/goldenchart/service/BacktestingService.java @@ -160,56 +160,18 @@ public class BacktestingService { double exitPrice = getPrice(series, req.getBars(), i, cfg.getExitPriceType()); long time = series.getBar(i).getEndTime().getEpochSecond(); - // ── SIGNAL_ONLY 모드: 지표 시그널은 포지션 무관, 체결·레포트는 LONG_ONLY와 동일 ── + // ── SIGNAL_ONLY 모드: 포지션·자금 상태 무관, 조건 충족 시마다 전체 시그널 생성 ── if (signalOnly) { - if (!inPosition) { - if (i - lastExitBar <= reentryWait && lastExitBar >= 0) { - if (useLedger) ledger.markToMarket(closePrice); - continue; - } - if (entryRule.isSatisfied(i)) { - double effEntry = applySlippage(closePrice, cfg, true); - double qty = enterPosition(ledger, useLedger, equity, tradeSizePct, - effEntry, closePrice, record, i, series, time); - if (qty > 0) { - String sigType = "SHORT".equals(direction) ? "SHORT_ENTRY" : "BUY"; - signals.add(buildFillSignal(time, sigType, effEntry, i, qty)); - entryPrice = effEntry; - entryBarIdx = i; - inPosition = true; - partialDone = false; - if (useLedger) equity = ledger.portfolioValue(closePrice); - } - } - } else { - if (partialExit && !partialDone && exitRule.isSatisfied(i)) { - double effExit = applySlippage(exitPrice, cfg, false); - double qty = applyExit(ledger, useLedger, equity, tradeSizePct, entryPrice, - effExit, partialPct, direction, cfg, simGrossProfit, simGrossLoss, time, i); - if (qty > 0) { - signals.add(buildFillSignal(time, "PARTIAL_SELL", effExit, i, qty)); - partialDone = true; - if (useLedger) equity = ledger.portfolioValue(closePrice); - } - } else if (exitRule.isSatisfied(i)) { - double effExit = applySlippage(exitPrice, cfg, false); - double size = partialDone ? (1.0 - partialPct) : 1.0; - double qty = applyExit(ledger, useLedger, equity, tradeSizePct, entryPrice, effExit, - size, direction, cfg, simGrossProfit, simGrossLoss, time, i); - if (qty > 0) { - Num numExitPrice = series.numFactory().numOf(effExit); - Num numShares = record.getCurrentPosition().getEntry().getAmount(); - record.exit(i, numExitPrice, numShares); - String sigType = "SHORT".equals(direction) ? "SHORT_EXIT" : "SELL"; - signals.add(buildFillSignal(time, sigType, effExit, i, qty)); - inPosition = false; - lastExitBar = i; - partialDone = false; - if (useLedger) equity = ledger.portfolioValue(closePrice); - } - } + if (entryRule.isSatisfied(i)) { + double effEntry = applySlippage(closePrice, cfg, true); + String buyType = "SHORT".equals(direction) ? "SHORT_ENTRY" : "BUY"; + signals.add(buildFillSignal(time, buyType, effEntry, i, 1.0)); + } + if (exitRule.isSatisfied(i)) { + double effExit = applySlippage(exitPrice, cfg, false); + String sellType = "SHORT".equals(direction) ? "SHORT_EXIT" : "SELL"; + signals.add(buildFillSignal(time, sellType, effExit, i, 1.0)); } - if (useLedger) ledger.markToMarket(closePrice); continue; } diff --git a/frontend/src/components/BacktestSettingsModal.tsx b/frontend/src/components/BacktestSettingsModal.tsx index b39f795..bcc00cd 100644 --- a/frontend/src/components/BacktestSettingsModal.tsx +++ b/frontend/src/components/BacktestSettingsModal.tsx @@ -15,6 +15,11 @@ interface FieldMeta { } const FIELD_META: Partial> = { + positionMode: { + label: '시그널 생성 방식', + description: + '백테스팅 시 매수·매도 시그널을 어떻게 생성할지 결정합니다.\n• 순수 지표 기준(SIGNAL_ONLY): 포지션·자금 보유 여부와 무관하게 전략 조건이 충족되는 모든 구간에서 시그널을 생성합니다. 지표 조건이 실제로 몇 번 발생하는지 확인할 때 사용합니다.\n• 실거래 기준(LONG_ONLY): 1회 매수 후 매도할 때까지 재매수를 차단합니다. 실제 자본 운용 시뮬레이션에 사용합니다.', + }, analysisMethod: { label: '투자분석 방식', description: @@ -134,6 +139,18 @@ interface SettingSection { } const SECTIONS: SettingSection[] = [ + { + title: '🎯 시그널 생성 방식', + fields: [ + { + key: 'positionMode', type: 'select', + opts: [ + { value: 'SIGNAL_ONLY', label: '순수 지표 기준 (모든 시그널 생성)' }, + { value: 'LONG_ONLY', label: '실거래 기준 (포지션 1회 제한)' }, + ], + }, + ], + }, { title: '📊 투자분석 방식', fields: [ diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index 23591fd..66f80c7 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -1286,7 +1286,7 @@ export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = { maxOpenTrades: 1, partialExitEnabled: false, partialExitPct: 50, - positionMode: 'LONG_ONLY', + positionMode: 'SIGNAL_ONLY', analysisMethod: 'MARK_TO_MARKET', };