전략평가 개선

This commit is contained in:
Macbook
2026-06-05 12:22:29 +09:00
parent 0aaf4ac19c
commit e0ef5845a2
5 changed files with 390 additions and 83 deletions
@@ -294,8 +294,26 @@ public class StrategyDslToTa4jAdapter {
java.time.Instant primaryEndTime = primarySeries.getBar(index).getEndTime();
evalIndex = findLastIndexAtOrBefore(branchSeries, primaryEndTime);
} else if (useConfirmedOnly) {
// CANDLE_CLOSE: 마지막 확정봉(provisional 제외)
evalIndex = Math.max(branchSeries.getBeginIndex(), branchSeries.getEndIndex() - 1);
// CANDLE_CLOSE — 타임스탬프 기반 확정봉 판정 (백테스트 룩업과 동일 로직)
//
// [버그 수정] 단순 getEndIndex()-1 을 사용하면 경계 시점(예: 5m=10:00, 1h=10:00 동시 마감)에
// 1h 봉이 이미 완전히 확정됐음에도 getEndIndex()-1 이 한 봉 이전(08:00-09:00)을 가리켜
// 백테스트와 1봉 차이가 발생했다. primaryEndTime 과 branchSeries 마지막 봉의 endTime 을
// 비교해 진짜 provisional 인지 이미 확정된 봉인지 구분한다.
//
// branchSeries.lastBar.endTime <= primary.endTime
// → 상위봉이 기본봉보다 이른 시점에 이미 마감된 경우 (=확정봉) → getEndIndex() 사용
// branchSeries.lastBar.endTime > primary.endTime
// → 상위봉 기간이 아직 열려 있는 경우 (=provisional) → getEndIndex()-1 사용
java.time.Instant primaryEndTime = primarySeries.getBar(index).getEndTime();
java.time.Instant branchLastEndTime = branchSeries.getLastBar().getEndTime();
if (!branchLastEndTime.isAfter(primaryEndTime)) {
// 상위봉이 기본봉 기준으로 이미 확정된 봉 → getEndIndex() 직접 사용
evalIndex = branchSeries.getEndIndex();
} else {
// 상위봉이 아직 형성 중(provisional) → 직전 확정봉 사용
evalIndex = Math.max(branchSeries.getBeginIndex(), branchSeries.getEndIndex() - 1);
}
} else {
// REALTIME_TICK: 현재 진행 중인 봉 포함
evalIndex = branchSeries.getEndIndex();
@@ -567,7 +585,8 @@ public class StrategyDslToTa4jAdapter {
try {
Indicator<Num> left = resolveField(leftField, indType, indParams, leftSeries, condPeriod, leftPeriod, cond, ctx);
Indicator<Num> right = resolveField(rightField, indType, indParams, rightSeries, condPeriod, rightPeriod, cond, ctx);
return new CrossTimeframeCompositeRule(condType, left, right, trigger, leftSeries, rightSeries, ctx.isBacktest());
return new CrossTimeframeCompositeRule(condType, left, right, trigger, leftSeries, rightSeries,
ctx.isBacktest(), ctx.useConfirmedOnly());
} catch (Exception e) {
log.warn("[Adapter] 복합 교차시간봉 빌드 실패 ind={} cond={}: {}",
indType, condType, e.getMessage());
@@ -575,12 +594,39 @@ public class StrategyDslToTa4jAdapter {
}
}
private static int evalIndex(BarSeries trigger, BarSeries branch, int triggerIndex, boolean isBacktest) {
/**
* [버그 수정] 라이브 CANDLE_CLOSE 시 상위봉 인덱스 결정 — CrossSeriesRule 과 동일 타임스탬프 비교 로직 적용.
*
* <p>기존 isBacktest=false 시 단순 {@code branch.getEndIndex()} 를 반환했는데,
* 이는 경계 시점(예: 5m=10:00, 1h=10:00 동시 마감)에 올바른 1h 봉을 가리키지만
* useConfirmedOnly=true 일 때 provisional 여부를 판단하지 않아 불일치 가능성이 있었다.
*
* <p>개선: isBacktest 여부와 무관하게 타임스탬프 비교로 통일한다.
* <ul>
* <li>backtest 또는 branch.lastEndTime ≤ trigger.endTime → 타임스탬프 기반 정확한 인덱스</li>
* <li>라이브 REALTIME_TICK (useConfirmedOnly=false) 이고 branch 가 아직 진행 중 → getEndIndex()</li>
* </ul>
*/
private static int evalIndex(BarSeries trigger, BarSeries branch, int triggerIndex,
boolean isBacktest, boolean useConfirmedOnly) {
if (trigger == branch) return triggerIndex;
java.time.Instant triggerEndTime = trigger.getBar(triggerIndex).getEndTime();
if (isBacktest) {
java.time.Instant t = trigger.getBar(triggerIndex).getEndTime();
return CrossSeriesRule.findLastIndexAtOrBefore(branch, t);
return CrossSeriesRule.findLastIndexAtOrBefore(branch, triggerEndTime);
}
if (useConfirmedOnly) {
// CANDLE_CLOSE — CrossSeriesRule 과 동일한 타임스탬프 비교 로직
java.time.Instant branchLastEndTime = branch.getLastBar().getEndTime();
if (!branchLastEndTime.isAfter(triggerEndTime)) {
return branch.getEndIndex();
} else {
return Math.max(branch.getBeginIndex(), branch.getEndIndex() - 1);
}
}
// REALTIME_TICK: 현재 진행 중인 봉 포함
int end = branch.getEndIndex();
return end >= 0 ? end : 0;
}
@@ -593,6 +639,7 @@ public class StrategyDslToTa4jAdapter {
private final BarSeries leftSeries;
private final BarSeries rightSeries;
private final boolean isBacktest;
private final boolean useConfirmedOnly;
CrossTimeframeCompositeRule(String condType,
Indicator<Num> left,
@@ -600,7 +647,8 @@ public class StrategyDslToTa4jAdapter {
BarSeries triggerSeries,
BarSeries leftSeries,
BarSeries rightSeries,
boolean isBacktest) {
boolean isBacktest,
boolean useConfirmedOnly) {
this.condType = condType;
this.left = left;
this.right = right;
@@ -608,12 +656,13 @@ public class StrategyDslToTa4jAdapter {
this.leftSeries = leftSeries;
this.rightSeries = rightSeries;
this.isBacktest = isBacktest;
this.useConfirmedOnly = useConfirmedOnly;
}
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
int li = evalIndex(triggerSeries, leftSeries, index, isBacktest);
int ri = evalIndex(triggerSeries, rightSeries, index, isBacktest);
int li = evalIndex(triggerSeries, leftSeries, index, isBacktest, useConfirmedOnly);
int ri = evalIndex(triggerSeries, rightSeries, index, isBacktest, useConfirmedOnly);
if (li < 1 || ri < 1) return false;
Num l0 = left.getValue(li - 1);