Skip to content

Commit ef895e5

Browse files
authored
Merge pull request #174 from CleanEngine/feat/chartdata
[BE]차트 데이터 Ohlcv 로직변경
2 parents f59eca7 + 31e0695 commit ef895e5

File tree

4 files changed

+144
-481
lines changed

4 files changed

+144
-481
lines changed

src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,51 +37,35 @@ public void publishRealTimeOhlc() {
3737
try {
3838
log.debug("△ 실시간 OHLC 데이터 스케줄러 실행");
3939

40-
// 구독된 티커가 없으면 조기 종료
4140
if (subscriptionService.getAllRealTimeOhlcSubscribedTickers().isEmpty()) {
4241
log.debug("실시간 OHLC 구독된 티커 없음, 전송 생략");
4342
return;
4443
}
44+
final LocalDateTime now = LocalDateTime.now();
4545

46-
// 모든 구독된 티커에 대해 데이터 전송
4746
for (String ticker : subscriptionService.getAllRealTimeOhlcSubscribedTickers()) {
4847
try {
4948
log.debug("티커 {} 실시간 OHLC 데이터 전송 중...", ticker);
5049

51-
// 티커별 최신 OHLC 데이터 조회 및 전송
52-
RealTimeOhlcDto ohlcData = realTimeOhlcService.getRealTimeOhlc(ticker);
50+
RealTimeOhlcDto ohlcData = realTimeOhlcService.getAndUpdateCumulative1mOhlc(ticker, now);
5351

5452
if (ohlcData == null) {
55-
// 이전에 전송한 데이터가 있는지 확인
5653
RealTimeOhlcDto lastSentData = lastSentOhlcDataMap.get(ticker);
57-
5854
if (lastSentData != null) {
59-
// 이전 데이터가 있으면 타임스탬프만 업데이트하여 재사용
6055
log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 이전 데이터 재사용", ticker);
61-
RealTimeOhlcDto updatedData = new RealTimeOhlcDto(lastSentData.getTicker(), LocalDateTime.now(), // 현재 시간으로 업데이트
56+
RealTimeOhlcDto updatedData = new RealTimeOhlcDto(lastSentData.getTicker(), now,
6257
lastSentData.getOpen(), lastSentData.getHigh(), lastSentData.getLow(), lastSentData.getClose(), lastSentData.getVolume());
63-
6458
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, updatedData);
65-
lastSentOhlcDataMap.put(ticker, updatedData); // 캐시 업데이트
59+
lastSentOhlcDataMap.put(ticker, updatedData);
6660
} else {
67-
// 이전 데이터도 없는 경우 빈 데이터 전송 (첫 구독 시)
6861
log.debug("티커 {}의 이전 OHLC 데이터도 없습니다. 빈 데이터 전송", ticker);
69-
RealTimeOhlcDto emptyData = new RealTimeOhlcDto();
70-
emptyData.setTicker(ticker);
71-
emptyData.setTimestamp(LocalDateTime.now());
72-
emptyData.setOpen(0.0);
73-
emptyData.setHigh(0.0);
74-
emptyData.setLow(0.0);
75-
emptyData.setClose(0.0);
76-
emptyData.setVolume(0.0);
77-
62+
RealTimeOhlcDto emptyData = new RealTimeOhlcDto(ticker, now, 0.0, 0.0, 0.0, 0.0, 0.0);
7863
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
79-
lastSentOhlcDataMap.put(ticker, emptyData); // 캐시 업데이트
64+
lastSentOhlcDataMap.put(ticker, emptyData);
8065
}
8166
} else {
82-
// 조회된 실시간 OHLC 데이터 전송
8367
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, ohlcData);
84-
lastSentOhlcDataMap.put(ticker, ohlcData); // 캐시 업데이트
68+
lastSentOhlcDataMap.put(ticker, ohlcData);
8569
log.debug("실시간 OHLC 데이터 전송: {}", ohlcData);
8670
}
8771
} catch (Exception e) {
@@ -91,8 +75,5 @@ public void publishRealTimeOhlc() {
9175
} catch (Exception e) {
9276
log.error("△ 실시간 OHLC 데이터 발행 중 오류: {}", e.getMessage(), e);
9377
}
94-
9578
}
96-
97-
9879
}

src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
44
import com.cleanengine.coin.chart.service.ChartSubscriptionService;
5-
import com.cleanengine.coin.chart.service.RealTimeOhlcService;
5+
import com.cleanengine.coin.chart.service.RealTimeOhlcService; // RealTimeOhlcService 의존성은 이제 불필요
66
import lombok.Getter;
77
import lombok.RequiredArgsConstructor;
88
import lombok.Setter;
@@ -19,9 +19,8 @@
1919
public class WebSocketMessageController {
2020

2121
private final ChartSubscriptionService subscriptionService;
22-
private final RealTimeOhlcService realTimeOhlcService;
2322
private final SimpMessagingTemplate messagingTemplate;
24-
private final ChartDataController chartDataController;
23+
private final ChartDataController chartDataController; // 이미 계산된 데이터를 가진 컨트롤러를 활용
2524

2625
/**
2726
* 실시간 OHLC 데이터 구독 처리
@@ -34,30 +33,16 @@ public void subscribeRealTimeOhlc(RealTimeTradeMappingDto request) {
3433
// 구독 목록에 추가
3534
subscriptionService.subscribeRealTimeOhlc(ticker);
3635

37-
// 구독 즉시 최근 실시간 OHLC 데이터 전송
38-
RealTimeOhlcDto latestOhlcData = realTimeOhlcService.getRealTimeOhlc(ticker);
39-
4036
RealTimeOhlcDto lastSentData = chartDataController.getLastSentOhlcDataMap().get(ticker);
4137

42-
if (latestOhlcData == null) {
43-
if (lastSentData != null) {
44-
// 이전에 전송한 데이터가 있으면 재사용
45-
log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 이전 데이터 재사용", ticker);
46-
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, lastSentData);
47-
} else {
48-
// 이전 데이터도 없는 경우 빈 데이터 전송
49-
log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 빈 데이터 전송", ticker);
50-
RealTimeOhlcDto emptyData = createEmptyRealTimeOhlcDto(ticker);
51-
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
52-
// 빈 데이터도 캐시에 저장
53-
chartDataController.getLastSentOhlcDataMap().put(ticker, emptyData);
54-
}
38+
if (lastSentData == null) {
39+
log.debug("티커 {}의 캐시된 OHLC 데이터가 없습니다. 빈 데이터 전송", ticker);
40+
RealTimeOhlcDto emptyData = createEmptyRealTimeOhlcDto(ticker);
41+
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
5542
} else {
56-
log.debug("티커 {}의 실시간 OHLC 데이터 전송: {}", ticker, latestOhlcData);
57-
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, latestOhlcData);
58-
// 데이터 캐시에 저장
59-
chartDataController.getLastSentOhlcDataMap().put(ticker, latestOhlcData);
60-
43+
// 캐시된 데이터가 있으면 즉시 전송
44+
log.debug("티커 {}의 캐시된 OHLC 데이터 즉시 전송: {}", ticker, lastSentData);
45+
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, lastSentData);
6146
}
6247
}
6348

@@ -80,6 +65,5 @@ private RealTimeOhlcDto createEmptyRealTimeOhlcDto(String ticker) {
8065
@Getter
8166
public static class RealTimeTradeMappingDto {
8267
private String ticker;
83-
8468
}
8569
}

src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java

Lines changed: 58 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.springframework.stereotype.Service;
1010

1111
import java.time.LocalDateTime;
12+
import java.time.temporal.ChronoUnit;
1213
import java.util.List;
1314
import java.util.Map;
1415
import java.util.concurrent.ConcurrentHashMap;
@@ -20,101 +21,86 @@ public class RealTimeOhlcService {
2021

2122
private final TradeRepository tradeRepository;
2223

23-
// 티커별 마지막 처리 시간
24-
private final Map<String, LocalDateTime> lastProcessedTimeMap = new ConcurrentHashMap<>();
2524

26-
// 티커별 마지막 OHLC 데이터 캐싱
27-
private final Map<String, RealTimeOhlcDto> lastOhlcDataMap = new ConcurrentHashMap<>();
25+
private final Map<String, RealTimeOhlcDto> currentMinuteOhlcCache = new ConcurrentHashMap<>();
2826

29-
/**
30-
* 특정 티커의 최신 1초 OHLC 데이터 생성
31-
*/
32-
public RealTimeOhlcDto getRealTimeOhlc(String ticker) {
33-
try {
34-
LocalDateTime now = LocalDateTime.now();
3527

36-
// 시간 범위 계산
37-
TimeRange timeRange = calculateTimeRange(ticker, now);
28+
public RealTimeOhlcDto getAndUpdateCumulative1mOhlc(String ticker, LocalDateTime now ) {
29+
try {
30+
LocalDateTime currentMinuteStart = now.truncatedTo(ChronoUnit.MINUTES);
3831

39-
// 거래 데이터 조회 및 전처리
40-
List<Trade> recentTrades = getProcessedTradeData(ticker, timeRange);
32+
RealTimeOhlcDto cachedOhlc = currentMinuteOhlcCache.get(ticker);
4133

42-
// 거래 데이터가 없으면 캐시된 데이터 반환
43-
if (recentTrades.isEmpty()) {
44-
return getCachedData(ticker);
34+
if (cachedOhlc == null || cachedOhlc.getTimestamp().isBefore(currentMinuteStart)) {
35+
return handleNewMinute(ticker, now, currentMinuteStart);
4536
}
37+
else {
38+
return handleExistingMinute(ticker, now, cachedOhlc);
39+
}
40+
} catch (Exception e) {
41+
log.error("티커 {}의 누적 OHLC 데이터 생성 중 오류 발생: {}", ticker, e.getMessage(), e);
42+
// 오류 발생 시 캐시된 마지막 데이터라도 반환
43+
return currentMinuteOhlcCache.get(ticker);
44+
}
45+
}
4646

47-
calculateOhlcv ohlcv = getCalculateOhlcv(recentTrades);
47+
private RealTimeOhlcDto handleNewMinute(String ticker, LocalDateTime now, LocalDateTime minuteStart) {
48+
log.debug("티커 {}: 새로운 1분봉 시작 ({}).", ticker, minuteStart);
49+
List<Trade> trades = tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(ticker, minuteStart, now);
4850

49-
RealTimeOhlcDto ohlcData = createOhlcDto(ticker, now, ohlcv);
51+
if (trades.isEmpty()) {
52+
return null;
53+
}
5054

51-
// 캐시 업데이트
52-
updateCache(ticker, now, ohlcData);
55+
// 새 거래내역으로 OHLCV 계산
56+
CalculateOhlcv ohlcv = getCalculateOhlcv(trades);
57+
RealTimeOhlcDto newOhlc = createOhlcDto(ticker, now, ohlcv);
5358

54-
return ohlcData;
55-
} catch (Exception e) {
56-
log.error("실시간 OHLC 데이터 생성 중 오류: {}", e.getMessage(), e);
57-
return getCachedData(ticker);
58-
}
59+
// 캐시를 새로운 1분봉 데이터로 교체
60+
currentMinuteOhlcCache.put(ticker, newOhlc);
61+
return newOhlc;
5962
}
6063

61-
// 시간 범위 계산
62-
TimeRange calculateTimeRange(String ticker, LocalDateTime now) {
63-
LocalDateTime lastProcessedTime = lastProcessedTimeMap.getOrDefault(
64-
ticker, now.minusSeconds(1));
65-
return new TimeRange(lastProcessedTime, now);
66-
}
6764

68-
// 거래 데이터 조회 및 전처리
69-
List<Trade> getProcessedTradeData(String ticker, TimeRange timeRange) {
70-
return tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(
71-
ticker,
72-
timeRange.start(),
73-
timeRange.end()
74-
);
75-
}
65+
private RealTimeOhlcDto handleExistingMinute(String ticker, LocalDateTime now, RealTimeOhlcDto cachedOhlc) {
66+
LocalDateTime lastProcessedTime = cachedOhlc.getTimestamp();
67+
List<Trade> newTrades = tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(ticker, lastProcessedTime, now);
7668

77-
// 캐시 업데이트
78-
void updateCache(String ticker, LocalDateTime now, RealTimeOhlcDto ohlcData) {
79-
lastProcessedTimeMap.put(ticker, now);
80-
lastOhlcDataMap.put(ticker, ohlcData);
81-
}
69+
// 새로운 거래가 없다면, 타임스탬프만 최신으로 업데이트하여 "살아있음"을 알림
70+
if (newTrades.isEmpty()) {
71+
cachedOhlc.setTimestamp(now);
72+
return cachedOhlc;
73+
}
8274

83-
// 캐시된 데이터 조회
84-
RealTimeOhlcDto getCachedData(String ticker) {
85-
return lastOhlcDataMap.getOrDefault(ticker, null);
86-
}
75+
log.trace("티커 {}: 기존 1분봉 업데이트. 신규 거래 {}건", ticker, newTrades.size());
8776

88-
// DTO 생성
89-
RealTimeOhlcDto createOhlcDto(String ticker, LocalDateTime timestamp, calculateOhlcv ohlcv) {
90-
return new RealTimeOhlcDto(
91-
ticker,
92-
timestamp,
93-
ohlcv.open(),
94-
ohlcv.high(),
95-
ohlcv.low(),
96-
ohlcv.close(),
97-
ohlcv.volume()
98-
);
77+
// Open(시가)는 분이 끝날때까지 고정
78+
cachedOhlc.setHigh(Math.max(cachedOhlc.getHigh(), newTrades.stream().mapToDouble(Trade::getPrice).max().orElse(cachedOhlc.getHigh())));
79+
cachedOhlc.setLow(Math.min(cachedOhlc.getLow(), newTrades.stream().mapToDouble(Trade::getPrice).min().orElse(cachedOhlc.getLow())));
80+
cachedOhlc.setClose(newTrades.getLast().getPrice()); // 종가는 항상 마지막 거래 가격
81+
cachedOhlc.setVolume(cachedOhlc.getVolume() + newTrades.stream().mapToDouble(Trade::getSize).sum());
82+
cachedOhlc.setTimestamp(now); // 마지막 처리 시간 갱신
83+
84+
return cachedOhlc;
9985
}
10086

101-
// OHLCV 계산 메서드
87+
88+
10289
@NotNull
103-
static calculateOhlcv getCalculateOhlcv(List<Trade> trades) {
104-
// trades는 시간 오름차순 정렬되어 있음
105-
Double open = trades.getFirst().getPrice(); // 첫 번째(가장 오래된) = Open ✅
106-
Double close = trades.getLast().getPrice(); // 마지막(가장 최근) = Close ✅
90+
private CalculateOhlcv getCalculateOhlcv(List<Trade> trades) {
91+
Double open = trades.getFirst().getPrice();
92+
Double close = trades.getLast().getPrice();
10793
Double high = trades.stream().mapToDouble(Trade::getPrice).max().orElse(0.0);
10894
Double low = trades.stream().mapToDouble(Trade::getPrice).min().orElse(0.0);
10995
Double volume = trades.stream().mapToDouble(Trade::getSize).sum();
110-
111-
return new calculateOhlcv(open, high, low, close, volume);
96+
return new CalculateOhlcv(open, high, low, close, volume);
11297
}
11398

99+
private RealTimeOhlcDto createOhlcDto(String ticker, LocalDateTime timestamp, CalculateOhlcv ohlcv) {
100+
return new RealTimeOhlcDto(
101+
ticker, timestamp, ohlcv.open(), ohlcv.high(), ohlcv.low(), ohlcv.close(), ohlcv.volume()
102+
);
103+
}
114104

115-
116-
117-
record TimeRange(LocalDateTime start, LocalDateTime end) {}
118-
119-
record calculateOhlcv(Double open, Double high, Double low, Double close, Double volume) {}
105+
record CalculateOhlcv(Double open, Double high, Double low, Double close, Double volume) {}
120106
}

0 commit comments

Comments
 (0)