종목변경 시 캔들차트 사라지는 현상 수정
This commit is contained in:
@@ -55,24 +55,6 @@ public class BarBuilder {
|
||||
/** 진행 중인 1분봉 상태 (market → PartialBar) */
|
||||
private final ConcurrentHashMap<String, PartialBar> partialBars = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 봉 마감 직후 전략 판정 시그널 임시 보관 (market → "BUY"|"SELL"|"NONE").
|
||||
* publishCurrentBar 에서 단 1회 실어 보낸 뒤 제거된다.
|
||||
*/
|
||||
private final ConcurrentHashMap<String, String> signalOnClose = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 시그널이 발생한 봉의 barStart epoch-second 임시 보관.
|
||||
* publishCurrentBar 에서 signal 과 함께 사용되고 제거된다.
|
||||
*/
|
||||
private final ConcurrentHashMap<String, Long> signalBarTimeEpoch = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 시그널이 발생한 봉의 최종 close 가격 임시 보관.
|
||||
* publishCurrentBar 에서 시그널 메시지의 price 로 사용된다.
|
||||
*/
|
||||
private final ConcurrentHashMap<String, Double> signalBarClose = new ConcurrentHashMap<>();
|
||||
|
||||
// ── 공개 API ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -136,7 +118,7 @@ public class BarBuilder {
|
||||
String evalCandleType = timeframeService.resolveCandleTypeForMarket(market);
|
||||
BarSeries series1m = ta4jStorage.getOrCreate(market, "1m");
|
||||
if ("1m".equals(evalCandleType)) {
|
||||
evaluateStrategyOnBarClose(market, "1m", series1m.getEndIndex(), partial, bar, true);
|
||||
evaluateStrategyOnBarClose(market, "1m", series1m.getEndIndex(), bar);
|
||||
}
|
||||
|
||||
// 상위 타임프레임 전파
|
||||
@@ -167,7 +149,8 @@ public class BarBuilder {
|
||||
BarSeries upperAfter = ta4jStorage.getOrCreate(market, type);
|
||||
if (type.equals(evalCandleType)) {
|
||||
int maturedUpper = Math.max(upperAfter.getBeginIndex(), upperAfter.getEndIndex() - 1);
|
||||
evaluateStrategyOnBarClose(market, type, maturedUpper, partial, bar, false);
|
||||
Bar maturedUpperBar = upperAfter.getBar(maturedUpper);
|
||||
evaluateStrategyOnBarClose(market, type, maturedUpper, maturedUpperBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,12 +158,10 @@ public class BarBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* 봉 마감 시점 전략 평가 + 시그널 저장 + 주문 큐 적재.
|
||||
*
|
||||
* @param publishStompSignal 1m 만 STOMP 시그널 필드에 실어 보냄
|
||||
* 봉 마감 시점 전략 평가 + 시그널 STOMP 발행(평가 분봉 채널) + DB/주문 큐 적재.
|
||||
*/
|
||||
private void evaluateStrategyOnBarClose(String market, String candleType, int maturedIndex,
|
||||
PartialBar partial, Bar bar, boolean publishStompSignal) {
|
||||
Bar signalBar) {
|
||||
String closeSignal = liveStrategyEvaluator.evaluateCandleClose(market, candleType, maturedIndex);
|
||||
String signalExecType = "CANDLE_CLOSE";
|
||||
|
||||
@@ -189,10 +170,8 @@ public class BarBuilder {
|
||||
signalExecType = "REALTIME_TICK";
|
||||
}
|
||||
|
||||
if (publishStompSignal) {
|
||||
signalOnClose.put(market, closeSignal);
|
||||
signalBarTimeEpoch.put(market, partial.barStart.toEpochSecond());
|
||||
signalBarClose.put(market, partial.close);
|
||||
if ("BUY".equals(closeSignal) || "SELL".equals(closeSignal)) {
|
||||
publishStrategySignal(market, candleType, signalBar, closeSignal);
|
||||
}
|
||||
|
||||
if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) return;
|
||||
@@ -202,70 +181,52 @@ public class BarBuilder {
|
||||
GcLiveStrategySettings activeSetting = liveSettingsRepo.findActiveByMarket(market)
|
||||
.stream().filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck())
|
||||
&& execType.equals(s.getExecutionType())).findFirst().orElse(null);
|
||||
long candleTimeEpoch = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
|
||||
long candleTimeEpoch = signalBar.getEndTime().getEpochSecond()
|
||||
- signalBar.getTimePeriod().getSeconds();
|
||||
String devId = activeSetting != null ? activeSetting.getDeviceId() : null;
|
||||
Long uid = activeSetting != null ? activeSetting.getUserId() : null;
|
||||
Long stratId = activeSetting != null ? activeSetting.getStrategyId() : null;
|
||||
tradeSignalService.save(
|
||||
devId, uid, market, stratId, null,
|
||||
closeSignal, partial.close, candleTimeEpoch, candleType, execType);
|
||||
closeSignal, signalBar.getClosePrice().doubleValue(),
|
||||
candleTimeEpoch, candleType, execType);
|
||||
if (devId != null) {
|
||||
orderExecutionQueue.submitSignal(devId, uid, market, stratId, closeSignal, partial.close);
|
||||
orderExecutionQueue.submitSignal(devId, uid, market, stratId, closeSignal,
|
||||
signalBar.getClosePrice().doubleValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[BarBuilder] 시그널 처리 실패 ({}/{}): {}", market, candleType, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 진행 중인 1분봉을 STOMP 로 발행.
|
||||
*
|
||||
* <p><b>핵심 버그 수정 (백테스팅 데이터 오염 방지)</b><br>
|
||||
* 분 전환 시점에 {@code commitBar} 가 {@code signalOnClose = "NONE"} 을 항상 설정하므로,
|
||||
* 이전 코드에서는 {@code signal != null} 조건만으로 {@code closedBarTime} 을 결정했다.
|
||||
* 그 결과 분 전환 첫 틱에 발행되는 STOMP 메시지가:
|
||||
* - time = 직전 확정봉 barStart (잘못된 타임스탬프)
|
||||
* - close = 새 provisional 봉 첫 틱 가격 (잘못된 close)
|
||||
* 가 되어 프론트엔드 차트에서 직전 확정봉의 close 를 새 봉의 첫 틱으로 덮어쓰게 된다.
|
||||
* 이 오염된 close 값은 백테스팅 전송 데이터로도 활용되어 CCI 계산이 왜곡된다.
|
||||
*
|
||||
* <p>수정 원칙:
|
||||
* <ul>
|
||||
* <li>실제 BUY/SELL 시그널 → 마감봉 시각(closedBarTime)과 마감봉 close 를 사용</li>
|
||||
* <li>"NONE" 또는 null → 현재 진행 중인 봉 시각(p.barStart)과 실시간 close 를 사용</li>
|
||||
* </ul>
|
||||
*/
|
||||
/** 평가 분봉 STOMP 채널로 봉 마감 시그널 발행 */
|
||||
private void publishStrategySignal(String market, String candleType, Bar bar, String signal) {
|
||||
long barStartEpoch = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
|
||||
CandleBarDto dto = CandleBarDto.builder()
|
||||
.time(barStartEpoch)
|
||||
.open(bar.getOpenPrice().doubleValue())
|
||||
.high(bar.getHighPrice().doubleValue())
|
||||
.low(bar.getLowPrice().doubleValue())
|
||||
.close(bar.getClosePrice().doubleValue())
|
||||
.volume(bar.getVolume().doubleValue())
|
||||
.signal(signal)
|
||||
.build();
|
||||
broker.publish(market, candleType, dto);
|
||||
log.info("[BarBuilder] bar-close signal={} market={} candleType={}", signal, market, candleType);
|
||||
}
|
||||
|
||||
/** 현재 진행 중인 1분봉을 STOMP 로 발행 (실시간 틱 렌더링용, 시그널 없음). */
|
||||
private void publishCurrentBar(String market) {
|
||||
PartialBar p = partialBars.get(market);
|
||||
if (p == null) return;
|
||||
|
||||
String signal = signalOnClose.remove(market);
|
||||
|
||||
// ★ "NONE" 은 실제 시그널이 아니므로 closedBarTime 을 사용하지 않는다.
|
||||
// 이전에는 signal != null 로만 판단해서 "NONE" 도 시각 보정 대상이 되었고,
|
||||
// 결과적으로 새 봉의 첫 틱 데이터가 직전 확정봉 시각으로 발행되어 차트 데이터가 오염됐다.
|
||||
boolean isRealSignal = "BUY".equals(signal) || "SELL".equals(signal);
|
||||
|
||||
Long closedBarTime = isRealSignal ? signalBarTimeEpoch.remove(market) : null;
|
||||
Double closedBarClose = isRealSignal ? signalBarClose.remove(market) : null;
|
||||
|
||||
// 잔여 맵 엔트리 정리 (실제 시그널 없는 경우)
|
||||
if (!isRealSignal) {
|
||||
signalBarTimeEpoch.remove(market);
|
||||
signalBarClose.remove(market);
|
||||
}
|
||||
|
||||
long barTime = (closedBarTime != null) ? closedBarTime : p.barStart.toEpochSecond();
|
||||
double barClose = (closedBarClose != null) ? closedBarClose : p.close;
|
||||
|
||||
CandleBarDto dto = CandleBarDto.builder()
|
||||
.time(barTime)
|
||||
.time(p.barStart.toEpochSecond())
|
||||
.open(p.open)
|
||||
.high(p.high)
|
||||
.low(p.low)
|
||||
.close(barClose)
|
||||
.close(p.close)
|
||||
.volume(p.volume)
|
||||
.signal(isRealSignal ? signal : null) // null 이면 @JsonInclude(NON_NULL) 로 직렬화 제외
|
||||
.build();
|
||||
broker.publish(market, "1m", dto);
|
||||
publishUpperTimeframes(market);
|
||||
|
||||
Reference in New Issue
Block a user