139 lines
6.2 KiB
Java
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());
|
|
}
|
|
}
|
|
}
|
|
}
|