Files
goldenChart/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java
T
2026-06-08 10:08:39 +09:00

139 lines
6.2 KiB
Java

package com.goldenchart.websocket;
import com.goldenchart.dto.CandleBarDto;
import com.goldenchart.dto.SignalEventDto;
import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
import com.goldenchart.service.LiveStrategyEvaluator;
import com.goldenchart.service.StrategyConditionTimeframeService;
import com.goldenchart.service.TradeSignalService;
import com.goldenchart.trading.pipeline.OrderExecutionQueue;
import com.goldenchart.storage.Ta4jStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ta4j.core.BarSeries;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 3초 주기 라운드 로빈 스케줄러.
*
* <p>설정 방식이 REALTIME_TICK 이고 isLiveCheck = true 인 종목에 대해
* 현재 진행 중인 캔들 인덱스(series.getEndIndex())로 전략을 평가하여
* BUY/SELL 시그널이 발생하면 STOMP 토픽으로 발행한다.
*
* <p>동시성 안전:
* <ul>
* <li>BarBuilder 의 partialBars 와 Ta4jStorage 는 ConcurrentHashMap 기반</li>
* <li>이 스케줄러는 단일 스레드로 돌기 때문에 중복 평가 없음</li>
* </ul>
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class LiveStrategyScheduler {
private final GcLiveStrategySettingsRepository settingsRepo;
private final LiveStrategyEvaluator evaluator;
private final Ta4jStorage ta4jStorage;
private final TradingWebSocketBroker broker;
private final SignalEventBroker signalEventBroker;
private final TradeSignalService tradeSignalService;
private final OrderExecutionQueue orderExecutionQueue;
private final StrategyConditionTimeframeService conditionTimeframes;
/**
* 3초마다 REALTIME_TICK 설정 종목을 순회하여 전략 판정 후 시그널 발행.
* fixedDelay=3000: 이전 실행이 끝난 뒤 3초 후 재실행 (오버랩 방지).
*/
@Scheduled(fixedDelay = 3000)
public void tick() {
List<GcLiveStrategySettings> activeSettings =
settingsRepo.findAllByIsLiveCheckTrueAndUserIdIsNotNull();
if (activeSettings.isEmpty()) return;
for (GcLiveStrategySettings s : activeSettings) {
if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
if (s.getStrategyId() == null) continue;
String market = s.getMarket();
for (String candleType : conditionTimeframes.collectForStrategy(s.getStrategyId())) {
evaluateRealtimeTickForMarket(s, market, candleType);
}
}
}
private void evaluateRealtimeTickForMarket(GcLiveStrategySettings s, String market,
String candleType) {
if (!ta4jStorage.exists(market, candleType)) return;
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
if (series.isEmpty()) return;
String signal = evaluator.evaluateSettingRealtimeTick(s, market, candleType);
if ("BUY".equals(signal) || "SELL".equals(signal)) {
// 현재 진행 중인 캔들 정보 조회
org.ta4j.core.Bar lastBar = series.getLastBar();
// Ta4j 0.22: getEndTime() → Instant; 시작 시간 = endTime - duration
long barStartEpoch = lastBar.getEndTime().getEpochSecond()
- lastBar.getTimePeriod().getSeconds();
CandleBarDto dto = CandleBarDto.builder()
.time(barStartEpoch)
.open(lastBar.getOpenPrice().doubleValue())
.high(lastBar.getHighPrice().doubleValue())
.low(lastBar.getLowPrice().doubleValue())
.close(lastBar.getClosePrice().doubleValue())
.volume(lastBar.getVolume().doubleValue())
.signal(signal)
.candleType(candleType)
.strategyId(s.getStrategyId())
.userId(s.getUserId())
.deviceId(s.getDeviceId())
.executionType("REALTIME_TICK")
.build();
broker.publish(market, candleType, dto);
// [항목5] 시그널 전용 토픽에 추가 발행
SignalEventDto signalEvent = SignalEventDto.builder()
.market(market)
.candleType(candleType)
.strategyId(s.getStrategyId())
.signalType(signal)
.price(lastBar.getClosePrice().doubleValue())
.candleTime(barStartEpoch)
.executionType("REALTIME_TICK")
.timestamp(System.currentTimeMillis())
.build();
signalEventBroker.publishToUser(s.getUserId(), s.getDeviceId(), signalEvent);
log.info("[LiveStrategyScheduler] REALTIME_TICK signal={} market={}", signal, market);
// DB 저장
try {
tradeSignalService.save(
s.getDeviceId(), s.getUserId(),
market, s.getStrategyId(), null,
signal,
lastBar.getClosePrice().doubleValue(),
barStartEpoch, candleType, "REALTIME_TICK"
);
if (s.getUserId() != null) {
String strategyName = tradeSignalService.resolveStrategyName(
s.getStrategyId(), null);
orderExecutionQueue.submitSignal(
s.getUserId(), market,
s.getStrategyId(), signal,
lastBar.getClosePrice().doubleValue(),
candleType, strategyName, "REALTIME_TICK");
}
} catch (Exception ex) {
log.warn("[LiveStrategyScheduler] 시그널 DB 저장 실패: {}", ex.getMessage());
}
}
}
}