전략평가 로직 개선
This commit is contained in:
@@ -95,63 +95,78 @@ public class BarBuilder {
|
||||
|
||||
// ── 내부 처리 ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** 완성된 1분봉을 Ta4jStorage 에 저장하고 상위 타임프레임에 전파 */
|
||||
/**
|
||||
* 완성된 1분봉을 Ta4jStorage 에 저장하고 상위 타임프레임에 전파한다.
|
||||
*
|
||||
* <p>[항목2] 2-Phase 처리 — 멀티 타임프레임 레이스 컨디션 해소:
|
||||
* <ul>
|
||||
* <li><b>Phase 1</b>: 1m 봉과 모든 상위봉을 Ta4jStorage 에 먼저 확정한다.</li>
|
||||
* <li><b>Phase 2</b>: 모든 저장이 완료된 후에야 전략 평가를 트리거한다.</li>
|
||||
* </ul>
|
||||
* 기존 방식(1m 저장 → 1m 전략 평가 → 상위봉 저장 → 상위봉 전략 평가)에서는
|
||||
* 1m 전략이 1h CrossSeriesRule 을 참조할 때 1h 봉이 아직 확정 전이어서
|
||||
* 잘못된 인덱스를 바라보는 레이스 컨디션이 있었다.
|
||||
*/
|
||||
private void commitBar(String market, PartialBar partial) {
|
||||
Duration d1m = Duration.ofMinutes(1);
|
||||
ZonedDateTime endTime = partial.barStart.plus(d1m);
|
||||
|
||||
// Ta4j Bar 생성
|
||||
Bar bar = buildTa4jBar(partial, d1m, endTime);
|
||||
|
||||
// 1분봉 Ta4jStorage 저장
|
||||
// ── Phase 1: 모든 타임프레임 스토리지 업데이트 ────────────────────────
|
||||
|
||||
ta4jStorage.addBar(market, "1m", bar);
|
||||
log.debug("[BarBuilder] 1m Bar 확정: {} @ {}", market, endTime);
|
||||
|
||||
// 마감된 상위봉 정보(타입, maturedIndex, bar) — Phase 2 에서 사용
|
||||
java.util.List<UpperCloseInfo> closedUppers = new java.util.ArrayList<>();
|
||||
|
||||
for (int i = 0; i < UPPER_MINUTES.length; i++) {
|
||||
int minutes = UPPER_MINUTES[i];
|
||||
String type = UPPER_TYPES[i];
|
||||
ZonedDateTime upperStart = alignToInterval(partial.barStart, minutes);
|
||||
ZonedDateTime upperEnd = upperStart.plusMinutes(minutes);
|
||||
|
||||
BarSeries upperSeries = ta4jStorage.getOrCreate(market, type);
|
||||
if (upperSeries.isEmpty()) {
|
||||
Bar upperBar = buildTa4jBar(partial, Duration.ofMinutes(minutes), upperEnd);
|
||||
ta4jStorage.addBar(market, type, upperBar);
|
||||
} else {
|
||||
Instant lastUpperEndInstant = upperSeries.getLastBar().getEndTime();
|
||||
Instant upperEndInstant = upperEnd.toInstant();
|
||||
if (lastUpperEndInstant.equals(upperEndInstant)) {
|
||||
ta4jStorage.updateLastBar(market, type, partial.close, partial.volume);
|
||||
} else if (lastUpperEndInstant.isBefore(upperEndInstant)) {
|
||||
// 상위봉 마감 → 새 봉 추가
|
||||
Bar upperBar = buildTa4jBar(partial, Duration.ofMinutes(minutes), upperEnd);
|
||||
ta4jStorage.addBar(market, type, upperBar);
|
||||
BarSeries upperAfter = ta4jStorage.getOrCreate(market, type);
|
||||
int maturedIdx = Math.max(upperAfter.getBeginIndex(), upperAfter.getEndIndex() - 1);
|
||||
closedUppers.add(new UpperCloseInfo(type, maturedIdx, upperAfter.getBar(maturedIdx)));
|
||||
} else {
|
||||
log.warn("[BarBuilder] 상위봉 endTime 역행 {} {} last={} expected={} — 갱신만 수행",
|
||||
market, type, lastUpperEndInstant, upperEndInstant);
|
||||
ta4jStorage.updateLastBar(market, type, partial.close, partial.volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 2: 모든 봉이 확정된 후 전략 평가 트리거 ─────────────────────
|
||||
|
||||
if (barCloseEvaluation.shouldEvaluateOnClose(market, "1m")) {
|
||||
BarSeries series1m = ta4jStorage.getOrCreate(market, "1m");
|
||||
barCloseEvaluation.onMaturedBarClose(market, "1m", series1m.getEndIndex(), bar);
|
||||
}
|
||||
|
||||
// 상위 타임프레임 전파
|
||||
for (int i = 0; i < UPPER_MINUTES.length; i++) {
|
||||
int minutes = UPPER_MINUTES[i];
|
||||
String type = UPPER_TYPES[i];
|
||||
// 해당 상위 봉의 시작 시간 계산
|
||||
ZonedDateTime upperStart = alignToInterval(partial.barStart, minutes);
|
||||
ZonedDateTime upperEnd = upperStart.plusMinutes(minutes);
|
||||
|
||||
BarSeries upperSeries = ta4jStorage.getOrCreate(market, type);
|
||||
if (upperSeries.isEmpty()) {
|
||||
// 새 상위봉 열기
|
||||
Bar upperBar = buildTa4jBar(partial, Duration.ofMinutes(minutes), upperEnd);
|
||||
ta4jStorage.addBar(market, type, upperBar);
|
||||
} else {
|
||||
Bar lastUpper = upperSeries.getLastBar();
|
||||
// Ta4j 0.22: getEndTime() → Instant
|
||||
Instant lastUpperEndInstant = lastUpper.getEndTime();
|
||||
Instant upperEndInstant = upperEnd.toInstant();
|
||||
if (lastUpperEndInstant.equals(upperEndInstant)) {
|
||||
// 동일 구간 → 가격 갱신
|
||||
ta4jStorage.updateLastBar(market, type, partial.close, partial.volume);
|
||||
} else if (lastUpperEndInstant.isBefore(upperEndInstant)) {
|
||||
// 다음 구간 → 새 Bar 추가 (이전 상위봉 마감)
|
||||
Bar upperBar = buildTa4jBar(partial, Duration.ofMinutes(minutes), upperEnd);
|
||||
ta4jStorage.addBar(market, type, upperBar);
|
||||
BarSeries upperAfter = ta4jStorage.getOrCreate(market, type);
|
||||
if (barCloseEvaluation.shouldEvaluateOnClose(market, type)) {
|
||||
int maturedUpper = Math.max(upperAfter.getBeginIndex(), upperAfter.getEndIndex() - 1);
|
||||
Bar maturedUpperBar = upperAfter.getBar(maturedUpper);
|
||||
barCloseEvaluation.onMaturedBarClose(market, type, maturedUpper, maturedUpperBar);
|
||||
}
|
||||
} else {
|
||||
log.warn("[BarBuilder] 상위봉 endTime 역행 {} {} last={} expected={} — 갱신만 수행",
|
||||
market, type, lastUpperEndInstant, upperEndInstant);
|
||||
ta4jStorage.updateLastBar(market, type, partial.close, partial.volume);
|
||||
}
|
||||
}
|
||||
for (UpperCloseInfo info : closedUppers) {
|
||||
if (barCloseEvaluation.shouldEvaluateOnClose(market, info.type())) {
|
||||
barCloseEvaluation.onMaturedBarClose(market, info.type(), info.maturedIdx(), info.bar());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Phase 1 에서 마감된 상위봉 정보를 Phase 2 로 전달하는 내부 레코드 */
|
||||
private record UpperCloseInfo(String type, int maturedIdx, Bar bar) {}
|
||||
|
||||
/** 현재 진행 중인 1분봉을 STOMP 로 발행 (실시간 틱 렌더링용, 시그널 없음). */
|
||||
private void publishCurrentBar(String market) {
|
||||
PartialBar p = partialBars.get(market);
|
||||
|
||||
Reference in New Issue
Block a user