매매실행 방식 설정 추가
This commit is contained in:
@@ -107,4 +107,11 @@ public class BacktestSettingsDto {
|
|||||||
*/
|
*/
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String analysisMethod = "MARK_TO_MARKET";
|
private String analysisMethod = "MARK_TO_MARKET";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SCAN_SIGNALS — 조건 스캔(봉 종가·DSL 충족, 차트 마커와 동일)
|
||||||
|
* BACKTEST_ENGINE — 슬리피지·손절/익절·진입/청산가 등 백테스트 설정 반영 (기본)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private String tradeExecutionMode = "BACKTEST_ENGINE";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ public class GcBacktestSettings {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String analysisMethod = "MARK_TO_MARKET";
|
private String analysisMethod = "MARK_TO_MARKET";
|
||||||
|
|
||||||
|
/** SCAN_SIGNALS | BACKTEST_ENGINE */
|
||||||
|
@Column(name = "trade_execution_mode", nullable = false, length = 32)
|
||||||
|
@Builder.Default
|
||||||
|
private String tradeExecutionMode = "BACKTEST_ENGINE";
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ public class BacktestSettingsService {
|
|||||||
PortfolioLedger.REALIZED_ONLY.equals(dto.getAnalysisMethod())
|
PortfolioLedger.REALIZED_ONLY.equals(dto.getAnalysisMethod())
|
||||||
? PortfolioLedger.REALIZED_ONLY
|
? PortfolioLedger.REALIZED_ONLY
|
||||||
: PortfolioLedger.MARK_TO_MARKET);
|
: PortfolioLedger.MARK_TO_MARKET);
|
||||||
|
entity.setTradeExecutionMode(
|
||||||
|
"SCAN_SIGNALS".equals(dto.getTradeExecutionMode())
|
||||||
|
? "SCAN_SIGNALS"
|
||||||
|
: "BACKTEST_ENGINE");
|
||||||
|
|
||||||
return toDto(repo.save(entity));
|
return toDto(repo.save(entity));
|
||||||
}
|
}
|
||||||
@@ -84,6 +88,10 @@ public class BacktestSettingsService {
|
|||||||
.partialExitPct(e.getPartialExitPct())
|
.partialExitPct(e.getPartialExitPct())
|
||||||
.positionMode(e.getPositionMode() != null ? e.getPositionMode() : "LONG_ONLY")
|
.positionMode(e.getPositionMode() != null ? e.getPositionMode() : "LONG_ONLY")
|
||||||
.analysisMethod(e.getAnalysisMethod() != null ? e.getAnalysisMethod() : PortfolioLedger.MARK_TO_MARKET)
|
.analysisMethod(e.getAnalysisMethod() != null ? e.getAnalysisMethod() : PortfolioLedger.MARK_TO_MARKET)
|
||||||
|
.tradeExecutionMode(
|
||||||
|
"SCAN_SIGNALS".equals(e.getTradeExecutionMode())
|
||||||
|
? "SCAN_SIGNALS"
|
||||||
|
: "BACKTEST_ENGINE")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ public class BacktestingService {
|
|||||||
|
|
||||||
private static final BacktestSettingsDto DEFAULT_SETTINGS = new BacktestSettingsDto();
|
private static final BacktestSettingsDto DEFAULT_SETTINGS = new BacktestSettingsDto();
|
||||||
|
|
||||||
|
/** 조건 스캔 — 종가·DSL 충족 (슬리피지·손절/익절 Rule 미적용) */
|
||||||
|
public static final String TRADE_EXEC_SCAN_SIGNALS = "SCAN_SIGNALS";
|
||||||
|
/** 백테스트 엔진 — 슬리피지·리스크 Rule·진입/청산가 반영 */
|
||||||
|
public static final String TRADE_EXEC_BACKTEST_ENGINE = "BACKTEST_ENGINE";
|
||||||
|
|
||||||
|
private static boolean isScanSignalsExecution(BacktestSettingsDto cfg) {
|
||||||
|
return cfg != null && TRADE_EXEC_SCAN_SIGNALS.equals(cfg.getTradeExecutionMode());
|
||||||
|
}
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────────────────
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public BacktestResponse run(BacktestRequest req) {
|
public BacktestResponse run(BacktestRequest req) {
|
||||||
@@ -152,6 +161,9 @@ public class BacktestingService {
|
|||||||
// ── 청산 규칙 합성 ────────────────────────────────────────────────────────
|
// ── 청산 규칙 합성 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private Rule buildExitRule(Rule baseExit, BarSeries series, BacktestSettingsDto cfg) {
|
private Rule buildExitRule(Rule baseExit, BarSeries series, BacktestSettingsDto cfg) {
|
||||||
|
if (isScanSignalsExecution(cfg)) {
|
||||||
|
return baseExit;
|
||||||
|
}
|
||||||
Rule result = baseExit;
|
Rule result = baseExit;
|
||||||
ClosePriceIndicator close = new ClosePriceIndicator(series);
|
ClosePriceIndicator close = new ClosePriceIndicator(series);
|
||||||
|
|
||||||
@@ -196,7 +208,8 @@ public class BacktestingService {
|
|||||||
|
|
||||||
double initCap = cfg.getInitialCapital() != null ? cfg.getInitialCapital().doubleValue() : 10_000_000.0;
|
double initCap = cfg.getInitialCapital() != null ? cfg.getInitialCapital().doubleValue() : 10_000_000.0;
|
||||||
double tradeSizePct = cfg.getTradeSizeValue() != null ? cfg.getTradeSizeValue().doubleValue() / 100.0 : 1.0;
|
double tradeSizePct = cfg.getTradeSizeValue() != null ? cfg.getTradeSizeValue().doubleValue() / 100.0 : 1.0;
|
||||||
boolean partialExit = Boolean.TRUE.equals(cfg.getPartialExitEnabled());
|
final boolean scanExec = isScanSignalsExecution(cfg);
|
||||||
|
boolean partialExit = !scanExec && Boolean.TRUE.equals(cfg.getPartialExitEnabled());
|
||||||
double partialPct = cfg.getPartialExitPct() != null ? cfg.getPartialExitPct().doubleValue() / 100.0 : 0.5;
|
double partialPct = cfg.getPartialExitPct() != null ? cfg.getPartialExitPct().doubleValue() / 100.0 : 0.5;
|
||||||
boolean partialDone = false;
|
boolean partialDone = false;
|
||||||
|
|
||||||
@@ -208,10 +221,12 @@ public class BacktestingService {
|
|||||||
|
|
||||||
int barCount = series.getBarCount();
|
int barCount = series.getBarCount();
|
||||||
int loopStart = Math.max(0, Math.min(evalStartIndex, barCount));
|
int loopStart = Math.max(0, Math.min(evalStartIndex, barCount));
|
||||||
|
final String entryPriceType = scanExec ? "CLOSE" : cfg.getEntryPriceType();
|
||||||
|
final String exitPriceType = scanExec ? "CLOSE" : cfg.getExitPriceType();
|
||||||
|
|
||||||
for (int i = loopStart; i < barCount; i++) {
|
for (int i = loopStart; i < barCount; i++) {
|
||||||
double closePrice = getPrice(series, req.getBars(), i, cfg.getEntryPriceType());
|
double closePrice = getPrice(series, req.getBars(), i, entryPriceType);
|
||||||
double exitPrice = getPrice(series, req.getBars(), i, cfg.getExitPriceType());
|
double exitPrice = getPrice(series, req.getBars(), i, exitPriceType);
|
||||||
long time = barStartEpoch(series, i);
|
long time = barStartEpoch(series, i);
|
||||||
|
|
||||||
// ── SIGNAL_ONLY: 조건 충족 봉마다 시그널 (live-conditions 충족과 동일 기준) ──
|
// ── SIGNAL_ONLY: 조건 충족 봉마다 시그널 (live-conditions 충족과 동일 기준) ──
|
||||||
@@ -727,6 +742,9 @@ public class BacktestingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double applySlippage(double price, BacktestSettingsDto cfg, boolean isBuy) {
|
private double applySlippage(double price, BacktestSettingsDto cfg, boolean isBuy) {
|
||||||
|
if (isScanSignalsExecution(cfg)) {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
double slip = cfg.getSlippageRate() != null ? cfg.getSlippageRate().doubleValue() : 0.0005;
|
double slip = cfg.getSlippageRate() != null ? cfg.getSlippageRate().doubleValue() : 0.0005;
|
||||||
return isBuy ? price * (1 + slip) : price * (1 - slip);
|
return isBuy ? price * (1 + slip) : price * (1 - slip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 매매 체결 방식: SCAN_SIGNALS(조건 스캔·종가) | BACKTEST_ENGINE(슬리피지·리스크 Rule)
|
||||||
|
ALTER TABLE gc_backtest_settings
|
||||||
|
ADD COLUMN trade_execution_mode VARCHAR(32) NOT NULL DEFAULT 'BACKTEST_ENGINE'
|
||||||
|
COMMENT 'SCAN_SIGNALS | BACKTEST_ENGINE'
|
||||||
|
AFTER analysis_method;
|
||||||
@@ -15,6 +15,11 @@ interface FieldMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FIELD_META: Partial<Record<keyof BacktestSettingsDto, FieldMeta>> = {
|
const FIELD_META: Partial<Record<keyof BacktestSettingsDto, FieldMeta>> = {
|
||||||
|
tradeExecutionMode: {
|
||||||
|
label: '매매 체결 방식',
|
||||||
|
description:
|
||||||
|
'시그널·체결가 산출 방식입니다.\n• 백테스트 엔진: 슬리피지, 손절/익절, 진입/청산가 반영. 실제 수익률 분석에 가깝습니다.\n• 조건 스캔: 봉 종가·DSL 충족 기준. 차트 마커·조건 패널과 동일합니다.',
|
||||||
|
},
|
||||||
positionMode: {
|
positionMode: {
|
||||||
label: '시그널 생성 방식',
|
label: '시그널 생성 방식',
|
||||||
description:
|
description:
|
||||||
@@ -139,6 +144,18 @@ interface SettingSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SECTIONS: SettingSection[] = [
|
const SECTIONS: SettingSection[] = [
|
||||||
|
{
|
||||||
|
title: '⚡ 매매 체결 방식',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'tradeExecutionMode', type: 'select',
|
||||||
|
opts: [
|
||||||
|
{ value: 'BACKTEST_ENGINE', label: '백테스트 엔진 (슬리피지·리스크 반영)' },
|
||||||
|
{ value: 'SCAN_SIGNALS', label: '조건 스캔 (봉 종가·DSL 충족)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '🎯 시그널 생성 방식',
|
title: '🎯 시그널 생성 방식',
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
saveBacktestSettings,
|
saveBacktestSettings,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import { ANALYSIS_METHOD_OPTIONS } from '../utils/analysisMethodLabels';
|
import { ANALYSIS_METHOD_OPTIONS } from '../utils/analysisMethodLabels';
|
||||||
|
import { TRADE_EXECUTION_MODE_OPTIONS } from '../utils/tradeExecutionMode';
|
||||||
|
|
||||||
// ── 백테스팅 전용 UI 컴포넌트 ───────────────────────────────────────────────
|
// ── 백테스팅 전용 UI 컴포넌트 ───────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -107,6 +108,36 @@ export const BacktestSettingsPanel: React.FC<BacktestSettingsPanelProps> = ({
|
|||||||
</BtGrid>
|
</BtGrid>
|
||||||
</BtSection>
|
</BtSection>
|
||||||
|
|
||||||
|
<BtSection title="매매 체결 방식">
|
||||||
|
<BtGrid>
|
||||||
|
<BtField
|
||||||
|
label="시그널 · 체결가 산출"
|
||||||
|
desc="전략평가·백테스팅·분석 레포트에 공통 적용됩니다."
|
||||||
|
full
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
className="stg-select stg-select--wide"
|
||||||
|
value={cfg.tradeExecutionMode ?? 'BACKTEST_ENGINE'}
|
||||||
|
onChange={e => field('tradeExecutionMode', e.target.value as 'SCAN_SIGNALS' | 'BACKTEST_ENGINE')}
|
||||||
|
>
|
||||||
|
{TRADE_EXECUTION_MODE_OPTIONS.map(o => (
|
||||||
|
<option key={o.value} value={o.value}>{o.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</BtField>
|
||||||
|
</BtGrid>
|
||||||
|
<p className="stg-bt-hint">
|
||||||
|
{(cfg.tradeExecutionMode ?? 'BACKTEST_ENGINE') === 'SCAN_SIGNALS'
|
||||||
|
? TRADE_EXECUTION_MODE_OPTIONS[1].desc
|
||||||
|
: TRADE_EXECUTION_MODE_OPTIONS[0].desc}
|
||||||
|
</p>
|
||||||
|
{(cfg.tradeExecutionMode ?? 'BACKTEST_ENGINE') === 'SCAN_SIGNALS' && (
|
||||||
|
<p className="stg-bt-hint stg-bt-hint--warn">
|
||||||
|
조건 스캔 모드: 슬리피지·손절/익절·트레일링스탑·진입/청산가(NEXT_OPEN) 설정은 시그널 생성에 적용되지 않습니다. 수수료만 시뮬레이션에 반영됩니다.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</BtSection>
|
||||||
|
|
||||||
<BtSection title="투자분석 방식">
|
<BtSection title="투자분석 방식">
|
||||||
<BtGrid>
|
<BtGrid>
|
||||||
<BtField
|
<BtField
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
padEvaluationSignalCount,
|
padEvaluationSignalCount,
|
||||||
} from '../utils/strategyEvaluationReport';
|
} from '../utils/strategyEvaluationReport';
|
||||||
import { resolveEvaluationCommissionRate } from '../utils/strategyEvaluationSignals';
|
import { resolveEvaluationCommissionRate } from '../utils/strategyEvaluationSignals';
|
||||||
|
import { normalizeTradeExecutionMode, tradeExecutionModeLabel } from '../utils/tradeExecutionMode';
|
||||||
import { BACKTEST_DISPLAY_BAR_COUNT, resolveEvaluationBarSlice } from '../utils/backtestWarmup';
|
import { BACKTEST_DISPLAY_BAR_COUNT, resolveEvaluationBarSlice } from '../utils/backtestWarmup';
|
||||||
import type { StrategyEvaluationWindowMeta } from './strategyEvaluation/StrategyEvaluationChart';
|
import type { StrategyEvaluationWindowMeta } from './strategyEvaluation/StrategyEvaluationChart';
|
||||||
import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
|
import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
|
||||||
@@ -452,6 +453,11 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
|||||||
[backtestSettings],
|
[backtestSettings],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tradeExecutionMode = useMemo(
|
||||||
|
() => normalizeTradeExecutionMode(backtestSettings?.tradeExecutionMode),
|
||||||
|
[backtestSettings],
|
||||||
|
);
|
||||||
|
|
||||||
const analysisSummary = useMemo(() => {
|
const analysisSummary = useMemo(() => {
|
||||||
if (!selectedStrategy) return null;
|
if (!selectedStrategy) return null;
|
||||||
return buildStrategyEvaluationToolbarSummary(backtestSignals, {
|
return buildStrategyEvaluationToolbarSummary(backtestSignals, {
|
||||||
@@ -749,6 +755,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
|||||||
firstBarClose: evalBars[0]?.close,
|
firstBarClose: evalBars[0]?.close,
|
||||||
lastBarClose: evalBars[evalBars.length - 1]?.close,
|
lastBarClose: evalBars[evalBars.length - 1]?.close,
|
||||||
commissionRate: evaluationCommissionRate,
|
commissionRate: evaluationCommissionRate,
|
||||||
|
tradeExecutionMode,
|
||||||
});
|
});
|
||||||
if (gen !== reportGenRef.current) return;
|
if (gen !== reportGenRef.current) return;
|
||||||
if (!model) {
|
if (!model) {
|
||||||
@@ -779,6 +786,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
|||||||
bars,
|
bars,
|
||||||
evalWindowMeta.evaluationBarCount,
|
evalWindowMeta.evaluationBarCount,
|
||||||
evaluationCommissionRate,
|
evaluationCommissionRate,
|
||||||
|
tradeExecutionMode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -819,7 +827,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
|||||||
{selectedStrategy && analysisSummary && !signalScanRunning && (
|
{selectedStrategy && analysisSummary && !signalScanRunning && (
|
||||||
<span
|
<span
|
||||||
className="seval-analysis-summary"
|
className="seval-analysis-summary"
|
||||||
title="일괄매수(최초 전액) · 분할매수(40/30/잔여) · 레포트와 동일 필터 기준"
|
title={`${tradeExecutionModeLabel(tradeExecutionMode)} · 일괄매수(최초 전액) · 분할매수(40/30/잔여) · 레포트와 동일 필터 기준`}
|
||||||
>
|
>
|
||||||
<span className="seval-analysis-item">
|
<span className="seval-analysis-item">
|
||||||
매수:<strong>{padEvaluationSignalCount(analysisSummary.buyCount)}</strong>건
|
매수:<strong>{padEvaluationSignalCount(analysisSummary.buyCount)}</strong>건
|
||||||
|
|||||||
@@ -1436,6 +1436,8 @@ export interface BacktestSettingsDto {
|
|||||||
positionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY';
|
positionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY';
|
||||||
/** MARK_TO_MARKET | REALIZED_ONLY — 투자분석 방식 */
|
/** MARK_TO_MARKET | REALIZED_ONLY — 투자분석 방식 */
|
||||||
analysisMethod?: 'MARK_TO_MARKET' | 'REALIZED_ONLY';
|
analysisMethod?: 'MARK_TO_MARKET' | 'REALIZED_ONLY';
|
||||||
|
/** SCAN_SIGNALS | BACKTEST_ENGINE — 매매 체결·시그널 생성 방식 */
|
||||||
|
tradeExecutionMode?: 'SCAN_SIGNALS' | 'BACKTEST_ENGINE';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = {
|
export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = {
|
||||||
@@ -1460,6 +1462,7 @@ export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = {
|
|||||||
partialExitPct: 50,
|
partialExitPct: 50,
|
||||||
positionMode: 'LONG_ONLY',
|
positionMode: 'LONG_ONLY',
|
||||||
analysisMethod: 'MARK_TO_MARKET',
|
analysisMethod: 'MARK_TO_MARKET',
|
||||||
|
tradeExecutionMode: 'BACKTEST_ENGINE',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 백테스팅 설정 로드 */
|
/** 백테스팅 설정 로드 */
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { EquityPoint, TradeHistoryRow } from './backtestEquity';
|
|||||||
import { repairUtf8Mojibake } from './textEncoding';
|
import { repairUtf8Mojibake } from './textEncoding';
|
||||||
import { getKoreanName } from './marketNameCache';
|
import { getKoreanName } from './marketNameCache';
|
||||||
import { toUpbitMarket } from './backtestUiUtils';
|
import { toUpbitMarket } from './backtestUiUtils';
|
||||||
|
import type { TradeExecutionMode } from './tradeExecutionMode';
|
||||||
|
|
||||||
export type BuyAllocationMode = 'full' | 'split';
|
export type BuyAllocationMode = 'full' | 'split';
|
||||||
|
|
||||||
@@ -357,6 +358,7 @@ export function buildStrategyEvaluationReportModel(opts: {
|
|||||||
firstBarClose?: number;
|
firstBarClose?: number;
|
||||||
lastBarClose?: number;
|
lastBarClose?: number;
|
||||||
commissionRate?: number;
|
commissionRate?: number;
|
||||||
|
tradeExecutionMode?: TradeExecutionMode;
|
||||||
}): BacktestAnalysisReportModel | null {
|
}): BacktestAnalysisReportModel | null {
|
||||||
const filtered = filterSignalsForEvaluationReport(opts.signals);
|
const filtered = filterSignalsForEvaluationReport(opts.signals);
|
||||||
const stillHolding = isStillHolding(filtered);
|
const stillHolding = isStillHolding(filtered);
|
||||||
@@ -396,6 +398,8 @@ export function buildStrategyEvaluationReportModel(opts: {
|
|||||||
reportSignalFilterNote:
|
reportSignalFilterNote:
|
||||||
'분석 기간 전체 캔들 기준으로, 최초 매수 이전 매도·최종 매도 이후 매수 시그널은 제외했습니다. '
|
'분석 기간 전체 캔들 기준으로, 최초 매수 이전 매도·최종 매도 이후 매수 시그널은 제외했습니다. '
|
||||||
+ '매도는 보유 물량 전량 매도로 가정합니다. '
|
+ '매도는 보유 물량 전량 매도로 가정합니다. '
|
||||||
+ '체결가·청산 Rule은 백테스트 설정(슬리피지·손절/익절·진입/청산가)과 동일합니다.',
|
+ (opts.tradeExecutionMode === 'SCAN_SIGNALS'
|
||||||
|
? '체결가: 조건 스캔(봉 종가·DSL 충족).'
|
||||||
|
: '체결가: 백테스트 설정(슬리피지·손절/익절·진입/청산가) 반영.'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* 전략평가 — 차트·상단 요약·분석 레포트·일괄평가 공통 시그널 소스
|
* 전략평가·일괄평가 — 백테스트 설정의 매매 체결 방식에 따라 시그널 조회
|
||||||
*
|
|
||||||
* runBacktest(SIGNAL_ONLY) + 백테스트 설정(슬리피지·손절/익절·진입/청산가) 기준.
|
|
||||||
* scan-signals(종가·이상적 체결)와 달리 실제 백테스트 엔진과 동일한 Rule·체결가를 사용한다.
|
|
||||||
*/
|
*/
|
||||||
import type { OHLCVBar } from '../types';
|
import type { OHLCVBar } from '../types';
|
||||||
import {
|
import {
|
||||||
|
fetchLiveConditionScanSignals,
|
||||||
loadBacktestSettings,
|
loadBacktestSettings,
|
||||||
runBacktest,
|
runBacktest,
|
||||||
type BacktestSettingsDto,
|
type BacktestSettingsDto,
|
||||||
@@ -13,12 +11,14 @@ import {
|
|||||||
type StrategyDto,
|
type StrategyDto,
|
||||||
} from './backendApi';
|
} from './backendApi';
|
||||||
import { buildEvalParamsFromStrategy } from './strategyEvaluationParams';
|
import { buildEvalParamsFromStrategy } from './strategyEvaluationParams';
|
||||||
|
import { isScanSignalsExecutionMode, normalizeTradeExecutionMode } from './tradeExecutionMode';
|
||||||
|
|
||||||
export interface StrategyEvaluationSignalsResult {
|
export interface StrategyEvaluationSignalsResult {
|
||||||
signals: BacktestSignal[];
|
signals: BacktestSignal[];
|
||||||
initialCapital: number;
|
initialCapital: number;
|
||||||
settings: BacktestSettingsDto;
|
settings: BacktestSettingsDto;
|
||||||
commissionRate: number;
|
commissionRate: number;
|
||||||
|
tradeExecutionMode: ReturnType<typeof normalizeTradeExecutionMode>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveEvaluationCommissionRate(settings: BacktestSettingsDto): number {
|
export function resolveEvaluationCommissionRate(settings: BacktestSettingsDto): number {
|
||||||
@@ -26,6 +26,17 @@ export function resolveEvaluationCommissionRate(settings: BacktestSettingsDto):
|
|||||||
return settings.commissionRate ?? 0.0015;
|
return settings.commissionRate ?? 0.0015;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapBarsForRequest(bars: OHLCVBar[]) {
|
||||||
|
return bars.map(b => ({
|
||||||
|
time: b.time,
|
||||||
|
open: b.open,
|
||||||
|
high: b.high,
|
||||||
|
low: b.low,
|
||||||
|
close: b.close,
|
||||||
|
volume: b.volume,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchStrategyEvaluationSignals(opts: {
|
export async function fetchStrategyEvaluationSignals(opts: {
|
||||||
strategyId: number;
|
strategyId: number;
|
||||||
strategy: StrategyDto;
|
strategy: StrategyDto;
|
||||||
@@ -37,19 +48,38 @@ export async function fetchStrategyEvaluationSignals(opts: {
|
|||||||
backtestSettings?: BacktestSettingsDto | null;
|
backtestSettings?: BacktestSettingsDto | null;
|
||||||
}): Promise<StrategyEvaluationSignalsResult> {
|
}): Promise<StrategyEvaluationSignalsResult> {
|
||||||
const btSettings = opts.backtestSettings ?? await loadBacktestSettings();
|
const btSettings = opts.backtestSettings ?? await loadBacktestSettings();
|
||||||
const settings: BacktestSettingsDto = { ...btSettings, positionMode: 'SIGNAL_ONLY' };
|
const tradeExecutionMode = normalizeTradeExecutionMode(btSettings.tradeExecutionMode);
|
||||||
const indicatorParams = buildEvalParamsFromStrategy(opts.strategy, opts.getParams);
|
const indicatorParams = buildEvalParamsFromStrategy(opts.strategy, opts.getParams);
|
||||||
|
const initialCapital = btSettings.initialCapital ?? 10_000_000;
|
||||||
|
const commissionRate = resolveEvaluationCommissionRate(btSettings);
|
||||||
|
|
||||||
|
if (isScanSignalsExecutionMode(btSettings)) {
|
||||||
|
const signals = await fetchLiveConditionScanSignals(
|
||||||
|
opts.market,
|
||||||
|
opts.strategyId,
|
||||||
|
mapBarsForRequest(opts.bars),
|
||||||
|
opts.timeframe,
|
||||||
|
indicatorParams,
|
||||||
|
opts.evaluationBarCount,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
signals,
|
||||||
|
initialCapital,
|
||||||
|
settings: { ...btSettings, positionMode: 'SIGNAL_ONLY', tradeExecutionMode },
|
||||||
|
commissionRate,
|
||||||
|
tradeExecutionMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings: BacktestSettingsDto = {
|
||||||
|
...btSettings,
|
||||||
|
positionMode: 'SIGNAL_ONLY',
|
||||||
|
tradeExecutionMode: 'BACKTEST_ENGINE',
|
||||||
|
};
|
||||||
|
|
||||||
const res = await runBacktest({
|
const res = await runBacktest({
|
||||||
strategyId: opts.strategyId,
|
strategyId: opts.strategyId,
|
||||||
bars: opts.bars.map(b => ({
|
bars: mapBarsForRequest(opts.bars),
|
||||||
time: b.time,
|
|
||||||
open: b.open,
|
|
||||||
high: b.high,
|
|
||||||
low: b.low,
|
|
||||||
close: b.close,
|
|
||||||
volume: b.volume,
|
|
||||||
})),
|
|
||||||
timeframe: opts.timeframe,
|
timeframe: opts.timeframe,
|
||||||
symbol: opts.market,
|
symbol: opts.market,
|
||||||
strategyName: opts.strategy.name,
|
strategyName: opts.strategy.name,
|
||||||
@@ -62,11 +92,11 @@ export async function fetchStrategyEvaluationSignals(opts: {
|
|||||||
throw new Error('시그널 계산 실패');
|
throw new Error('시그널 계산 실패');
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialCapital = btSettings.initialCapital ?? res.analysis?.initialCapital ?? 10_000_000;
|
|
||||||
return {
|
return {
|
||||||
signals: res.signals,
|
signals: res.signals,
|
||||||
initialCapital,
|
initialCapital: btSettings.initialCapital ?? res.analysis?.initialCapital ?? initialCapital,
|
||||||
settings,
|
settings,
|
||||||
commissionRate: resolveEvaluationCommissionRate(settings),
|
commissionRate,
|
||||||
|
tradeExecutionMode: 'BACKTEST_ENGINE',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { BacktestSettingsDto } from './backendApi';
|
||||||
|
|
||||||
|
export type TradeExecutionMode = 'SCAN_SIGNALS' | 'BACKTEST_ENGINE';
|
||||||
|
|
||||||
|
export const TRADE_EXECUTION_MODE_OPTIONS: {
|
||||||
|
value: TradeExecutionMode;
|
||||||
|
label: string;
|
||||||
|
desc: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
value: 'BACKTEST_ENGINE',
|
||||||
|
label: '백테스트 엔진 (슬리피지·리스크 반영)',
|
||||||
|
desc: '슬리피지, 손절/익절, 진입/청산가 기준. 실제 수익률 분석에 가깝습니다.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'SCAN_SIGNALS',
|
||||||
|
label: '조건 스캔 (봉 종가·DSL 충족)',
|
||||||
|
desc: '차트 마커·조건 패널과 동일. 봉 종가 기준 이상적 체결.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function normalizeTradeExecutionMode(
|
||||||
|
value?: string | null,
|
||||||
|
): TradeExecutionMode {
|
||||||
|
return value === 'SCAN_SIGNALS' ? 'SCAN_SIGNALS' : 'BACKTEST_ENGINE';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isScanSignalsExecutionMode(
|
||||||
|
settings?: Pick<BacktestSettingsDto, 'tradeExecutionMode'> | null,
|
||||||
|
): boolean {
|
||||||
|
return normalizeTradeExecutionMode(settings?.tradeExecutionMode) === 'SCAN_SIGNALS';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tradeExecutionModeLabel(mode?: string | null): string {
|
||||||
|
const normalized = normalizeTradeExecutionMode(mode);
|
||||||
|
return TRADE_EXECUTION_MODE_OPTIONS.find(o => o.value === normalized)?.label
|
||||||
|
?? TRADE_EXECUTION_MODE_OPTIONS[0].label;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user