전략평가 오류 수정

This commit is contained in:
Macbook
2026-06-16 00:30:25 +09:00
parent db3513a63b
commit 133e3d0413
8 changed files with 176 additions and 54 deletions
@@ -49,21 +49,27 @@ public class BacktestingController {
}
long userId = TradingControllerSupport.requireRegisteredUser(headers);
String deviceId = headers.get("x-device-id");
if (deviceId == null || deviceId.isBlank()) {
deviceId = req.getDeviceId();
}
req.setUserId(userId);
req.setDeviceId(deviceId);
java.util.Map<String, java.util.Map<String, Object>> dbParams =
indicatorSettingsService.getAll(userId, null);
if (!dbParams.isEmpty()) {
java.util.Map<String, java.util.Map<String, Object>> merged =
new java.util.HashMap<>(dbParams);
if (req.getIndicatorParams() != null) {
req.getIndicatorParams().forEach((type, params) ->
merged.merge(type, params, (dbP, reqP) -> {
java.util.Map<String, Object> m = new java.util.HashMap<>(dbP);
m.putAll(reqP);
return m;
})
);
}
indicatorSettingsService.getAll(userId, deviceId);
java.util.Map<String, java.util.Map<String, Object>> merged =
new java.util.HashMap<>(dbParams);
if (req.getIndicatorParams() != null) {
req.getIndicatorParams().forEach((type, params) ->
merged.merge(type, params, (dbP, reqP) -> {
java.util.Map<String, Object> m = new java.util.HashMap<>(dbP);
m.putAll(reqP);
return m;
})
);
}
if (!merged.isEmpty()) {
req.setIndicatorParams(merged);
}
@@ -63,4 +63,7 @@ public class BacktestRequest {
/** 디바이스 ID (결과 저장용) */
private String deviceId;
/** 인증 사용자 ID — 컨트롤러에서 설정, 지표 설정 조회용 */
private Long userId;
}
@@ -77,13 +77,16 @@ public class BacktestingService {
}
}
// DB 저장 인디케이터 설정을 기본으로 로드 (디바이스별 저장값 → 하드코딩 기본값 방지)
// DB 저장 인디케이터 설정 — live-conditions/evaluate 와 동일 (userId + deviceId)
Map<String, Map<String, Object>> dbParams = Map.of();
Map<String, Map<String, Object>> visual = Map.of();
if (req.getDeviceId() != null && !req.getDeviceId().isBlank()) {
try {
dbParams = indicatorSettingsService.getAll(null, req.getDeviceId());
dbParams = indicatorSettingsService.getAll(req.getUserId(), req.getDeviceId());
visual = indicatorSettingsService.getAllVisual(req.getUserId(), req.getDeviceId());
} catch (Exception e) {
log.warn("[Backtest] 인디케이터 설정 DB 로드 실패 (deviceId={}): {}", req.getDeviceId(), e.getMessage());
log.warn("[Backtest] 인디케이터 설정 DB 로드 실패 (userId={}, deviceId={}): {}",
req.getUserId(), req.getDeviceId(), e.getMessage());
}
}
// 요청에 포함된 파라미터가 있으면 DB 값 위에 덮어씀 (요청값 우선)
@@ -111,9 +114,12 @@ public class BacktestingService {
Map<String, BarSeries> seriesOverrides = buildSeriesOverrides(
series, primaryTf, buyDsl, sellDsl);
Rule entryRule = adapter.toRule(buyDsl, series, params, seriesOverrides);
StrategyDslToTa4jAdapter.RuleBuildContext ruleCtx =
new StrategyDslToTa4jAdapter.RuleBuildContext(
series, params, visual, null, null, false, seriesOverrides);
Rule entryRule = adapter.toRule(buyDsl, ruleCtx);
Rule baseExitRule = (sellDsl != null && !sellDsl.isNull())
? adapter.toRule(sellDsl, series, params, seriesOverrides)
? adapter.toRule(sellDsl, ruleCtx)
: new BooleanRule(false);
Rule exitRule = buildExitRule(baseExitRule, series, cfg);
@@ -199,23 +205,21 @@ public class BacktestingService {
double exitPrice = getPrice(series, req.getBars(), i, cfg.getExitPriceType());
long time = barStartEpoch(series, i);
// ── SIGNAL_ONLY: 조건 충족 '시작' 봉만 시그널 (GTE 유지형 연속 중복 방지) ──
// ── SIGNAL_ONLY: 조건 충족 봉마다 시그널 (live-conditions 충족과 동일 기준) ──
if (signalOnly) {
boolean entrySatisfied = entryRule.isSatisfied(i);
boolean exitSatisfied = exitRule.isSatisfied(i);
boolean entryEdge = entrySatisfied && (i <= loopStart || !entryRule.isSatisfied(i - 1));
boolean exitEdge = exitSatisfied && (i <= loopStart || !exitRule.isSatisfied(i - 1));
if (entryEdge) {
if (entrySatisfied) {
double effEntry = applySlippage(closePrice, cfg, true);
String buyType = "SHORT".equals(direction) ? "SHORT_ENTRY" : "BUY";
signals.add(buildFillSignal(time, buyType, effEntry, i, 1.0));
log.debug("[Backtest:SIGNAL_ONLY] BUY(edge) @ bar={} price={}", i, effEntry);
log.debug("[Backtest:SIGNAL_ONLY] BUY @ bar={} price={}", i, effEntry);
}
if (exitEdge) {
if (exitSatisfied) {
double effExit = applySlippage(exitPrice, cfg, false);
String sellType = "SHORT".equals(direction) ? "SHORT_EXIT" : "SELL";
signals.add(buildFillSignal(time, sellType, effExit, i, 1.0));
log.debug("[Backtest:SIGNAL_ONLY] SELL(edge) @ bar={} price={}", i, effExit);
log.debug("[Backtest:SIGNAL_ONLY] SELL @ bar={} price={}", i, effExit);
}
continue;
}
@@ -57,8 +57,8 @@ public class StrategySignalDeterminer {
int index, String positionMode) {
try {
if ("SIGNAL_ONLY".equals(positionMode)) {
boolean enterOk = entryRule != null && isRisingEdge(entryRule, index);
boolean exitOk = exitRule != null && isRisingEdge(exitRule, index);
boolean enterOk = entryRule != null && entryRule.isSatisfied(index);
boolean exitOk = exitRule != null && exitRule.isSatisfied(index);
if (enterOk) return "BUY";
if (exitOk) return "SELL";
return "NONE";
@@ -84,15 +84,10 @@ public class StrategySignalDeterminer {
}
private String determineSignalOnly(Strategy strategy, int index) {
if (isRisingEdge(strategy.getEntryRule(), index)) return "BUY";
if (isRisingEdge(strategy.getExitRule(), index)) return "SELL";
Rule entry = strategy.getEntryRule();
Rule exit = strategy.getExitRule();
if (entry != null && entry.isSatisfied(index)) return "BUY";
if (exit != null && exit.isSatisfied(index)) return "SELL";
return "NONE";
}
/** GTE 등 유지형 조건 — 충족 시작 봉만 true (연속 시그널 방지) */
private boolean isRisingEdge(Rule rule, int index) {
if (rule == null || !rule.isSatisfied(index)) return false;
if (index <= 0) return true;
return !rule.isSatisfied(index - 1);
}
}