백테스팅 워밍업데이터 적용
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user