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', };