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초 주기 라운드 로빈 스케줄러. * *

설정 방식이 REALTIME_TICK 이고 isLiveCheck = true 인 종목에 대해 * 현재 진행 중인 캔들 인덱스(series.getEndIndex())로 전략을 평가하여 * BUY/SELL 시그널이 발생하면 STOMP 토픽으로 발행한다. * *

동시성 안전: *

*/ @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 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()); } } } }