전략평가 개선

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
@@ -5,6 +5,7 @@ import com.goldenchart.service.BarCloseStrategyEvaluationService;
import com.goldenchart.storage.Ta4jStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.ta4j.core.Bar;
import org.ta4j.core.BarSeries;
@@ -18,6 +19,7 @@ import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
/**
* 업비트 WebSocket 틱(Trade) 데이터를 수신하여 분봉 단위 캔들을 조립하는 빌더.
@@ -39,6 +41,14 @@ public class BarBuilder {
private final BarCloseStrategyEvaluationService barCloseEvaluation;
private final com.goldenchart.service.PaperOrderMatcherService paperOrderMatcher;
/**
* [항목4] Phase 2 비동기 실행용 스레드 풀.
* 웹소켓 수신 스레드가 전략 평가 연산에 블로킹되지 않도록 분리한다.
*/
@org.springframework.beans.factory.annotation.Autowired
@Qualifier("barCloseEvalExecutor")
private Executor barCloseEvalExecutor;
/** 상위 집계 타임프레임 (분 단위 집계 주기: 3, 5, 10, 15, 30, 60, 240, 1440) */
private static final int[] UPPER_MINUTES = {3, 5, 10, 15, 30, 60, 240, 10080, 1440};
private static final String[] UPPER_TYPES = {"3m", "5m", "10m", "15m", "30m", "1h", "4h", "1w", "1d"};
@@ -150,18 +160,34 @@ public class BarBuilder {
}
}
// ── Phase 2: 모든 봉이 확정된 후 전략 평가 트리거 ─────────────────────
// ── Phase 2: 모든 봉이 확정된 후 전략 평가 트리거 (비동기) ──────────────
//
// [항목4] 웹소켓 수신 스레드(업비트 연결 유지 담당)가 전략 평가 연산 때문에 블로킹되면
// 다음 분봉 시작 틱을 제때 수신하지 못할 수 있다. Phase 1 완료 후 Phase 2 를
// 전용 스레드 풀(barCloseEvalExecutor)에 제출하여 WebSocket 파이프라인을 즉시 반환한다.
//
// 스레드 안전성: Ta4jStorage·positionOpenCache 는 ConcurrentHashMap, maturedIdx 는
// Phase 1 에서 이미 확정된 값이므로 비동기 실행 중 Phase 1 이 재실행되어 인덱스가 바뀌어도
// 올바른 봉을 참조한다.
if (barCloseEvaluation.shouldEvaluateOnClose(market, "1m")) {
BarSeries series1m = ta4jStorage.getOrCreate(market, "1m");
barCloseEvaluation.onMaturedBarClose(market, "1m", series1m.getEndIndex(), bar);
}
for (UpperCloseInfo info : closedUppers) {
if (barCloseEvaluation.shouldEvaluateOnClose(market, info.type())) {
barCloseEvaluation.onMaturedBarClose(market, info.type(), info.maturedIdx(), info.bar());
final Bar confirmedBar = bar;
final java.util.List<UpperCloseInfo> closedUppersSnap
= java.util.Collections.unmodifiableList(closedUppers);
barCloseEvalExecutor.execute(() -> {
try {
if (barCloseEvaluation.shouldEvaluateOnClose(market, "1m")) {
BarSeries series1m = ta4jStorage.getOrCreate(market, "1m");
barCloseEvaluation.onMaturedBarClose(market, "1m", series1m.getEndIndex(), confirmedBar);
}
for (UpperCloseInfo info : closedUppersSnap) {
if (barCloseEvaluation.shouldEvaluateOnClose(market, info.type())) {
barCloseEvaluation.onMaturedBarClose(market, info.type(), info.maturedIdx(), info.bar());
}
}
} catch (Exception e) {
log.error("[BarBuilder] Phase 2 전략 평가 오류 market={}: {}", market, e.getMessage(), e);
}
}
});
}
/** Phase 1 에서 마감된 상위봉 정보를 Phase 2 로 전달하는 내부 레코드 */