백테스팅 워밍업데이터 적용

This commit is contained in:
Macbook
2026-06-12 01:41:34 +09:00
parent 0d338fffe4
commit 74b0ea4ab6
12 changed files with 335 additions and 52 deletions
@@ -117,7 +117,21 @@ public class BacktestingService {
: new BooleanRule(false);
Rule exitRule = buildExitRule(baseExitRule, series, cfg);
return runBacktest(series, entryRule, exitRule, req, cfg, strategyName, buyDsl, sellDsl, params);
int evalStart = resolveEvalStartIndex(req, n);
if (evalStart > 0) {
log.info("[Backtest] 지표 워밍업 {}봉, 평가 구간 {}봉 (index {}..{})",
evalStart, n - evalStart, evalStart, n - 1);
}
return runBacktest(series, entryRule, exitRule, req, cfg, strategyName, buyDsl, sellDsl, params, evalStart);
}
/** bars 앞쪽 워밍업 구간을 건너뛰고 마지막 evaluationBarCount 봉만 평가 */
private static int resolveEvalStartIndex(BacktestRequest req, int totalBars) {
if (req.getEvaluationBarCount() == null || totalBars <= 0) return 0;
int evalCount = req.getEvaluationBarCount();
if (evalCount <= 0 || evalCount >= totalBars) return 0;
return totalBars - evalCount;
}
// ── 청산 규칙 합성 ────────────────────────────────────────────────────────
@@ -147,7 +161,8 @@ public class BacktestingService {
BacktestRequest req, BacktestSettingsDto cfg,
String strategyName,
JsonNode execBuyDsl, JsonNode execSellDsl,
Map<String, Map<String, Object>> execParams) {
Map<String, Map<String, Object>> execParams,
int evalStartIndex) {
BaseTradingRecord record = new BaseTradingRecord();
@@ -177,8 +192,9 @@ public class BacktestingService {
final double[] simGrossLoss = {0.0};
int barCount = series.getBarCount();
int loopStart = Math.max(0, Math.min(evalStartIndex, barCount));
for (int i = 0; i < barCount; i++) {
for (int i = loopStart; i < barCount; i++) {
double closePrice = getPrice(series, req.getBars(), i, cfg.getEntryPriceType());
double exitPrice = getPrice(series, req.getBars(), i, cfg.getExitPriceType());
long time = barStartEpoch(series, i);
@@ -611,7 +627,9 @@ public class BacktestingService {
Map<String, Map<String, Object>> execParams) {
try {
List<OhlcvBar> bars = req.getBars();
long fromTime = bars.isEmpty() ? 0 : bars.get(0).getTime();
int evalStart = resolveEvalStartIndex(req, bars.size());
int evalCount = bars.isEmpty() ? 0 : bars.size() - evalStart;
long fromTime = bars.isEmpty() ? 0 : bars.get(evalStart).getTime();
long toTime = bars.isEmpty() ? 0 : bars.get(bars.size() - 1).getTime();
Map<String, Object> snapshot = new LinkedHashMap<>();
@@ -620,7 +638,14 @@ public class BacktestingService {
snapshot.put("symbol", req.getSymbol() != null ? req.getSymbol() : "UNKNOWN");
String execTf = normalizeTf(req.getTimeframe());
snapshot.put("timeframe", execTf);
snapshot.put("barCount", bars.size());
snapshot.put("barCount", evalCount);
if (evalStart > 0) {
snapshot.put("warmupBarCount", evalStart);
snapshot.put("totalBarsFetched", bars.size());
}
if (req.getEvaluationBarCount() != null) {
snapshot.put("evaluationBarCount", req.getEvaluationBarCount());
}
if (execParams != null && !execParams.isEmpty()) snapshot.put("indicatorParams", execParams);
if (execBuyDsl != null && !execBuyDsl.isNull()) snapshot.put("buyCondition", execBuyDsl);
if (execSellDsl != null && !execSellDsl.isNull()) snapshot.put("sellCondition", execSellDsl);
@@ -631,7 +656,7 @@ public class BacktestingService {
.strategyName(strategyName)
.symbol(req.getSymbol() != null ? req.getSymbol() : "UNKNOWN")
.timeframe(execTf)
.barCount(bars.size())
.barCount(evalCount)
.fromTime(fromTime)
.toTime(toTime)
.settingsJson(objectMapper.writeValueAsString(cfg))
@@ -45,6 +45,9 @@ public class HistoricalDataService {
@Value("${upbit.api.candle-limit:200}")
private int candleLimit;
/** 백테스트·차트 워밍업 포함 최대 history 요청 봉 수 */
private static final int MAX_HISTORY_FETCH = 800;
@Value("${upbit.api.request-delay-ms:110}")
private long requestDelayMs;
@@ -66,7 +69,7 @@ public class HistoricalDataService {
* @return CandleBarDto 리스트 (시간 오름차순)
*/
public List<CandleBarDto> getHistory(String market, String candleType, String to, int count) {
int safeCount = Math.min(count, candleLimit);
int safeCount = Math.min(Math.max(count, 1), MAX_HISTORY_FETCH);
// ── 1. to 시간 파싱 ───────────────────────────────────────────────────
ZonedDateTime toTime = parseToTime(to);
@@ -81,14 +84,14 @@ public class HistoricalDataService {
rawBars = sliceFromMemory(market, candleType, toTime, safeCount);
}
// 인메모리 봉이 최소 임계치(20개) 미만이면 Upbit REST 로 재조회.
// 백엔드 재시작 직후 warm-up 미완료 상태에서 2~3개 봉만 반환되는
// race-condition 을 방지한다.
// 인메모리 봉이 요청 수 미만이면 Upbit REST 로 보충 (백테스트 200+워밍업 등)
final int MIN_BARS_THRESHOLD = 20;
if (rawBars.size() < Math.min(MIN_BARS_THRESHOLD, safeCount)) {
log.info("[HistoricalDataService] 인메모리 봉 부족({}) → 업비트 REST 프록시: {} / {}",
rawBars.size(), market, candleType);
List<UpbitCandleRaw> upbitBars = fetchFromUpbit(market, candleType, to, safeCount);
if (rawBars.size() < safeCount
&& (rawBars.size() < Math.min(MIN_BARS_THRESHOLD, safeCount) || safeCount > candleLimit)) {
log.info("[HistoricalDataService] 봉 부족({}/{}) → 업비트 REST: {} / {}",
rawBars.size(), safeCount, market, candleType);
String toParam = to != null && !to.isBlank() ? to : null;
List<UpbitCandleRaw> upbitBars = fetchFromUpbit(market, candleType, toParam, safeCount);
if (upbitBars.size() > rawBars.size()) {
rawBars = upbitBars;
}