매매실행 방식 설정 추가
This commit is contained in:
@@ -1436,6 +1436,8 @@ export interface BacktestSettingsDto {
|
||||
positionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY';
|
||||
/** 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 = {
|
||||
@@ -1460,6 +1462,7 @@ export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = {
|
||||
partialExitPct: 50,
|
||||
positionMode: 'LONG_ONLY',
|
||||
analysisMethod: 'MARK_TO_MARKET',
|
||||
tradeExecutionMode: 'BACKTEST_ENGINE',
|
||||
};
|
||||
|
||||
/** 백테스팅 설정 로드 */
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { EquityPoint, TradeHistoryRow } from './backtestEquity';
|
||||
import { repairUtf8Mojibake } from './textEncoding';
|
||||
import { getKoreanName } from './marketNameCache';
|
||||
import { toUpbitMarket } from './backtestUiUtils';
|
||||
import type { TradeExecutionMode } from './tradeExecutionMode';
|
||||
|
||||
export type BuyAllocationMode = 'full' | 'split';
|
||||
|
||||
@@ -357,6 +358,7 @@ export function buildStrategyEvaluationReportModel(opts: {
|
||||
firstBarClose?: number;
|
||||
lastBarClose?: number;
|
||||
commissionRate?: number;
|
||||
tradeExecutionMode?: TradeExecutionMode;
|
||||
}): BacktestAnalysisReportModel | null {
|
||||
const filtered = filterSignalsForEvaluationReport(opts.signals);
|
||||
const stillHolding = isStillHolding(filtered);
|
||||
@@ -396,6 +398,8 @@ export function buildStrategyEvaluationReportModel(opts: {
|
||||
reportSignalFilterNote:
|
||||
'분석 기간 전체 캔들 기준으로, 최초 매수 이전 매도·최종 매도 이후 매수 시그널은 제외했습니다. '
|
||||
+ '매도는 보유 물량 전량 매도로 가정합니다. '
|
||||
+ '체결가·청산 Rule은 백테스트 설정(슬리피지·손절/익절·진입/청산가)과 동일합니다.',
|
||||
+ (opts.tradeExecutionMode === 'SCAN_SIGNALS'
|
||||
? '체결가: 조건 스캔(봉 종가·DSL 충족).'
|
||||
: '체결가: 백테스트 설정(슬리피지·손절/익절·진입/청산가) 반영.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/**
|
||||
* 전략평가 — 차트·상단 요약·분석 레포트·일괄평가 공통 시그널 소스
|
||||
*
|
||||
* runBacktest(SIGNAL_ONLY) + 백테스트 설정(슬리피지·손절/익절·진입/청산가) 기준.
|
||||
* scan-signals(종가·이상적 체결)와 달리 실제 백테스트 엔진과 동일한 Rule·체결가를 사용한다.
|
||||
* 전략평가·일괄평가 — 백테스트 설정의 매매 체결 방식에 따라 시그널 조회
|
||||
*/
|
||||
import type { OHLCVBar } from '../types';
|
||||
import {
|
||||
fetchLiveConditionScanSignals,
|
||||
loadBacktestSettings,
|
||||
runBacktest,
|
||||
type BacktestSettingsDto,
|
||||
@@ -13,12 +11,14 @@ import {
|
||||
type StrategyDto,
|
||||
} from './backendApi';
|
||||
import { buildEvalParamsFromStrategy } from './strategyEvaluationParams';
|
||||
import { isScanSignalsExecutionMode, normalizeTradeExecutionMode } from './tradeExecutionMode';
|
||||
|
||||
export interface StrategyEvaluationSignalsResult {
|
||||
signals: BacktestSignal[];
|
||||
initialCapital: number;
|
||||
settings: BacktestSettingsDto;
|
||||
commissionRate: number;
|
||||
tradeExecutionMode: ReturnType<typeof normalizeTradeExecutionMode>;
|
||||
}
|
||||
|
||||
export function resolveEvaluationCommissionRate(settings: BacktestSettingsDto): number {
|
||||
@@ -26,6 +26,17 @@ export function resolveEvaluationCommissionRate(settings: BacktestSettingsDto):
|
||||
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: {
|
||||
strategyId: number;
|
||||
strategy: StrategyDto;
|
||||
@@ -37,19 +48,38 @@ export async function fetchStrategyEvaluationSignals(opts: {
|
||||
backtestSettings?: BacktestSettingsDto | null;
|
||||
}): Promise<StrategyEvaluationSignalsResult> {
|
||||
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 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({
|
||||
strategyId: opts.strategyId,
|
||||
bars: opts.bars.map(b => ({
|
||||
time: b.time,
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
})),
|
||||
bars: mapBarsForRequest(opts.bars),
|
||||
timeframe: opts.timeframe,
|
||||
symbol: opts.market,
|
||||
strategyName: opts.strategy.name,
|
||||
@@ -62,11 +92,11 @@ export async function fetchStrategyEvaluationSignals(opts: {
|
||||
throw new Error('시그널 계산 실패');
|
||||
}
|
||||
|
||||
const initialCapital = btSettings.initialCapital ?? res.analysis?.initialCapital ?? 10_000_000;
|
||||
return {
|
||||
signals: res.signals,
|
||||
initialCapital,
|
||||
initialCapital: btSettings.initialCapital ?? res.analysis?.initialCapital ?? initialCapital,
|
||||
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