백테스팅 SIGNAL_ONLY 수정 — 포지션 무관 전체 시그널 생성 + 기본값 적용
- 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 <cursoragent@cursor.com>
This commit is contained in:
@@ -90,7 +90,7 @@ public class BacktestSettingsDto {
|
|||||||
// ── 포지션 종속성 모드 ─────────────────────────────────────────────────────
|
// ── 포지션 종속성 모드 ─────────────────────────────────────────────────────
|
||||||
/** "LONG_ONLY" | "SIGNAL_ONLY" — 매도 시그널 포지션 종속성 제어 */
|
/** "LONG_ONLY" | "SIGNAL_ONLY" — 매도 시그널 포지션 종속성 제어 */
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String positionMode = "LONG_ONLY";
|
private String positionMode = "SIGNAL_ONLY";
|
||||||
|
|
||||||
// ── 분할 청산 ──────────────────────────────────────────────────────────────
|
// ── 분할 청산 ──────────────────────────────────────────────────────────────
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
|
|||||||
@@ -160,56 +160,18 @@ public class BacktestingService {
|
|||||||
double exitPrice = getPrice(series, req.getBars(), i, cfg.getExitPriceType());
|
double exitPrice = getPrice(series, req.getBars(), i, cfg.getExitPriceType());
|
||||||
long time = series.getBar(i).getEndTime().getEpochSecond();
|
long time = series.getBar(i).getEndTime().getEpochSecond();
|
||||||
|
|
||||||
// ── SIGNAL_ONLY 모드: 지표 시그널은 포지션 무관, 체결·레포트는 LONG_ONLY와 동일 ──
|
// ── SIGNAL_ONLY 모드: 포지션·자금 상태 무관, 조건 충족 시마다 전체 시그널 생성 ──
|
||||||
if (signalOnly) {
|
if (signalOnly) {
|
||||||
if (!inPosition) {
|
|
||||||
if (i - lastExitBar <= reentryWait && lastExitBar >= 0) {
|
|
||||||
if (useLedger) ledger.markToMarket(closePrice);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (entryRule.isSatisfied(i)) {
|
if (entryRule.isSatisfied(i)) {
|
||||||
double effEntry = applySlippage(closePrice, cfg, true);
|
double effEntry = applySlippage(closePrice, cfg, true);
|
||||||
double qty = enterPosition(ledger, useLedger, equity, tradeSizePct,
|
String buyType = "SHORT".equals(direction) ? "SHORT_ENTRY" : "BUY";
|
||||||
effEntry, closePrice, record, i, series, time);
|
signals.add(buildFillSignal(time, buyType, effEntry, i, 1.0));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
if (exitRule.isSatisfied(i)) {
|
||||||
} else {
|
|
||||||
if (partialExit && !partialDone && exitRule.isSatisfied(i)) {
|
|
||||||
double effExit = applySlippage(exitPrice, cfg, false);
|
double effExit = applySlippage(exitPrice, cfg, false);
|
||||||
double qty = applyExit(ledger, useLedger, equity, tradeSizePct, entryPrice,
|
String sellType = "SHORT".equals(direction) ? "SHORT_EXIT" : "SELL";
|
||||||
effExit, partialPct, direction, cfg, simGrossProfit, simGrossLoss, time, i);
|
signals.add(buildFillSignal(time, sellType, effExit, i, 1.0));
|
||||||
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 (useLedger) ledger.markToMarket(closePrice);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ interface FieldMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FIELD_META: Partial<Record<keyof BacktestSettingsDto, FieldMeta>> = {
|
const FIELD_META: Partial<Record<keyof BacktestSettingsDto, FieldMeta>> = {
|
||||||
|
positionMode: {
|
||||||
|
label: '시그널 생성 방식',
|
||||||
|
description:
|
||||||
|
'백테스팅 시 매수·매도 시그널을 어떻게 생성할지 결정합니다.\n• 순수 지표 기준(SIGNAL_ONLY): 포지션·자금 보유 여부와 무관하게 전략 조건이 충족되는 모든 구간에서 시그널을 생성합니다. 지표 조건이 실제로 몇 번 발생하는지 확인할 때 사용합니다.\n• 실거래 기준(LONG_ONLY): 1회 매수 후 매도할 때까지 재매수를 차단합니다. 실제 자본 운용 시뮬레이션에 사용합니다.',
|
||||||
|
},
|
||||||
analysisMethod: {
|
analysisMethod: {
|
||||||
label: '투자분석 방식',
|
label: '투자분석 방식',
|
||||||
description:
|
description:
|
||||||
@@ -134,6 +139,18 @@ interface SettingSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SECTIONS: SettingSection[] = [
|
const SECTIONS: SettingSection[] = [
|
||||||
|
{
|
||||||
|
title: '🎯 시그널 생성 방식',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'positionMode', type: 'select',
|
||||||
|
opts: [
|
||||||
|
{ value: 'SIGNAL_ONLY', label: '순수 지표 기준 (모든 시그널 생성)' },
|
||||||
|
{ value: 'LONG_ONLY', label: '실거래 기준 (포지션 1회 제한)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '📊 투자분석 방식',
|
title: '📊 투자분석 방식',
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
@@ -1286,7 +1286,7 @@ export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = {
|
|||||||
maxOpenTrades: 1,
|
maxOpenTrades: 1,
|
||||||
partialExitEnabled: false,
|
partialExitEnabled: false,
|
||||||
partialExitPct: 50,
|
partialExitPct: 50,
|
||||||
positionMode: 'LONG_ONLY',
|
positionMode: 'SIGNAL_ONLY',
|
||||||
analysisMethod: 'MARK_TO_MARKET',
|
analysisMethod: 'MARK_TO_MARKET',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user