diff --git a/build.gradle b/build.gradle index 83ce59b6..f01680fb 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.16.0") + implementation 'org.hibernate.orm:hibernate-micrometer' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.13.1' diff --git a/docker/mariadb/init.sql b/docker/mariadb/init.sql index 29b57767..eb9362b2 100644 --- a/docker/mariadb/init.sql +++ b/docker/mariadb/init.sql @@ -110,3 +110,5 @@ INSERT INTO `if`.asset (ticker, name) VALUES ('TRUMP', '오피셜트럼프'); INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (1, 1, 0, 0, 500000000, 'BTC'); INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (2, 1, 0, 0, 500000000, 'TRUMP'); +INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (3, 2, 0, 0, 500000000, 'BTC'); +INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (4, 2, 0, 0, 500000000, 'TRUMP'); diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml index 1337a0b1..6e6431d4 100644 --- a/monitoring/docker-compose.yml +++ b/monitoring/docker-compose.yml @@ -9,7 +9,7 @@ services: - /etc/localtime:/etc/localtime:ro - ./opentelemetry-javaagent.jar:/app/opentelemetry-javaagent.jar working_dir: /app - command: [ "java", "-jar", "coin-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev,mariadb-local,actuator,apm" ] + command: [ "java", "-jar", "coin-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev,it,mariadb-local,actuator,apm" ] ports: - "8080:8080" env_file: @@ -18,14 +18,16 @@ services: - TZ=Asia/Seoul - OTEL_SERVICE_NAME=my-spring-app - OTEL_TRACES_EXPORTER=otlp - - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 - OTEL_LOGS_EXPORTER=none - OTEL_METRICS_EXPORTER=none - OTEL_INSTRUMENTATION_METHODS_ENABLED=true - - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005, -javaagent:/app/opentelemetry-javaagent.jar + - JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar depends_on: mariadb: condition: service_healthy + otel-collector: + condition: service_started networks: - app-network - monitoring-net @@ -60,12 +62,31 @@ services: command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.retention.time=30d' + - '--web.enable-remote-write-receiver' ports: - "9090:9090" networks: - monitoring-net restart: unless-stopped + influxdb: + image: influxdb:2.7 + container_name: influxdb + ports: + - "8086:8086" + volumes: + - influxdb_data:/var/lib/influxdb2 + environment: + - DOCKER_INFLUXDB_INIT_MODE=setup + - DOCKER_INFLUXDB_INIT_USERNAME=k6-user # 임의의 사용자/비밀번호 + - DOCKER_INFLUXDB_INIT_PASSWORD=k6-password + - DOCKER_INFLUXDB_INIT_ORG=k6-org + - DOCKER_INFLUXDB_INIT_BUCKET=k6-bucket + - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=vJvTx5b8vjgH7mu-M-BsEvdy5_COovexAjyniVhMF1yPzvsp2g8kp62opqXVMq6ICAq2tLJxxD6ifXPBZ9YVmA== + networks: + - monitoring-net + restart: unless-stopped + grafana: image: grafana/grafana:11.0.0 container_name: grafana @@ -80,22 +101,53 @@ services: depends_on: - prometheus - jaeger + - influxdb jaeger: image: jaegertracing/all-in-one:latest container_name: jaeger + user: "${UID}:${GID}" + environment: + - SPAN_STORAGE_TYPE=badger + - BADGER_EPHEMERAL=false + - BADGER_DIRECTORY_VALUE=/tmp/jaeger/data + - BADGER_DIRECTORY_KEY=/tmp/jaeger/keys + - COLLECTOR_OTLP_ENABLED=true + - COLLECTOR_OTLP_GRPC_HOST_PORT=0.0.0.0:4317 + - COLLECTOR_OTLP_HTTP_HOST_PORT=0.0.0.0:4318 + volumes: + - jaeger_data:/tmp/jaeger ports: - - "16686:16686" + - "16686:16686" # UI + - "4319:4317" + - "4320:4318" + networks: + - monitoring-net + + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yml"] + volumes: + - ./otel/otel-collector-config.yml:/etc/otel-collector-config.yml + ports: + - "8889:8889" - "4317:4317" - "4318:4318" + - "13133:13133" networks: - monitoring-net + depends_on: + jaeger: + condition: service_started volumes: prometheus_data: {} grafana_data: {} mariadb_data: driver: local + influxdb_data: {} + jaeger_data: {} networks: app-network: @@ -104,3 +156,8 @@ networks: monitoring-net: name: monitoring-net driver: bridge + + + # export K6_INFLUXDB_ORGANIZATION=k6-org + # - export K6_INFLUXDB_BUCKET=k6-bucket + # ./k6 run --out xk6-influxdb=http://localhost:8086 /Users/jangbongjun/coin/src/test/resources/k6/chart-stomp-test.js diff --git a/monitoring/grafana/provisioning/datsources/datasource.yml b/monitoring/grafana/provisioning/datasources/datasource.yml similarity index 100% rename from monitoring/grafana/provisioning/datsources/datasource.yml rename to monitoring/grafana/provisioning/datasources/datasource.yml diff --git a/monitoring/otel/otel-collector-config.yml b/monitoring/otel/otel-collector-config.yml new file mode 100644 index 00000000..c5852c05 --- /dev/null +++ b/monitoring/otel/otel-collector-config.yml @@ -0,0 +1,47 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 500ms + send_batch_size: 8192 + send_batch_max_size: 16384 + probabilistic_sampler: + sampling_percentage: 1 + +connectors: + spanmetrics: + histogram: + explicit: + buckets: [100us, 1ms, 2ms, 6ms, 10ms, 100ms, 250ms, 500ms, 1s] + +exporters: + otlp/jaeger: + endpoint: jaeger:4317 + tls: + insecure: true + + prometheus: + endpoint: "0.0.0.0:8889" + +service: + pipelines: + traces/metrics: + receivers: [otlp] + processors: [batch] + exporters: [spanmetrics] + + traces/jaeger: + receivers: [otlp] + processors: [probabilistic_sampler,batch] + exporters: [otlp/jaeger] + + metrics: + receivers: [spanmetrics] + processors: [batch] + exporters: [prometheus] \ No newline at end of file diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml index d1690101..59130832 100644 --- a/monitoring/prometheus/prometheus.yml +++ b/monitoring/prometheus/prometheus.yml @@ -8,4 +8,8 @@ scrape_configs: - job_name: 'my-app' static_configs: - targets: [ 'app:8080' ] - metrics_path: /actuator/prometheus \ No newline at end of file + metrics_path: /actuator/prometheus + - job_name: 'otel-collector' + scrape_interval: 15s + static_configs: + - targets: [ 'otel-collector:8889' ] \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java b/src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java index b13f113c..de5789cb 100644 --- a/src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java +++ b/src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java @@ -37,51 +37,35 @@ public void publishRealTimeOhlc() { try { log.debug("△ 실시간 OHLC 데이터 스케줄러 실행"); - // 구독된 티커가 없으면 조기 종료 if (subscriptionService.getAllRealTimeOhlcSubscribedTickers().isEmpty()) { log.debug("실시간 OHLC 구독된 티커 없음, 전송 생략"); return; } + final LocalDateTime now = LocalDateTime.now(); - // 모든 구독된 티커에 대해 데이터 전송 for (String ticker : subscriptionService.getAllRealTimeOhlcSubscribedTickers()) { try { log.debug("티커 {} 실시간 OHLC 데이터 전송 중...", ticker); - // 티커별 최신 OHLC 데이터 조회 및 전송 - RealTimeOhlcDto ohlcData = realTimeOhlcService.getRealTimeOhlc(ticker); + RealTimeOhlcDto ohlcData = realTimeOhlcService.getAndUpdateCumulative1mOhlc(ticker, now); if (ohlcData == null) { - // 이전에 전송한 데이터가 있는지 확인 RealTimeOhlcDto lastSentData = lastSentOhlcDataMap.get(ticker); - if (lastSentData != null) { - // 이전 데이터가 있으면 타임스탬프만 업데이트하여 재사용 log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 이전 데이터 재사용", ticker); - RealTimeOhlcDto updatedData = new RealTimeOhlcDto(lastSentData.getTicker(), LocalDateTime.now(), // 현재 시간으로 업데이트 + RealTimeOhlcDto updatedData = new RealTimeOhlcDto(lastSentData.getTicker(), now, lastSentData.getOpen(), lastSentData.getHigh(), lastSentData.getLow(), lastSentData.getClose(), lastSentData.getVolume()); - messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, updatedData); - lastSentOhlcDataMap.put(ticker, updatedData); // 캐시 업데이트 + lastSentOhlcDataMap.put(ticker, updatedData); } else { - // 이전 데이터도 없는 경우 빈 데이터 전송 (첫 구독 시) log.debug("티커 {}의 이전 OHLC 데이터도 없습니다. 빈 데이터 전송", ticker); - RealTimeOhlcDto emptyData = new RealTimeOhlcDto(); - emptyData.setTicker(ticker); - emptyData.setTimestamp(LocalDateTime.now()); - emptyData.setOpen(0.0); - emptyData.setHigh(0.0); - emptyData.setLow(0.0); - emptyData.setClose(0.0); - emptyData.setVolume(0.0); - + RealTimeOhlcDto emptyData = new RealTimeOhlcDto(ticker, now, 0.0, 0.0, 0.0, 0.0, 0.0); messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData); - lastSentOhlcDataMap.put(ticker, emptyData); // 캐시 업데이트 + lastSentOhlcDataMap.put(ticker, emptyData); } } else { - // 조회된 실시간 OHLC 데이터 전송 messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, ohlcData); - lastSentOhlcDataMap.put(ticker, ohlcData); // 캐시 업데이트 + lastSentOhlcDataMap.put(ticker, ohlcData); log.debug("실시간 OHLC 데이터 전송: {}", ohlcData); } } catch (Exception e) { @@ -91,8 +75,5 @@ public void publishRealTimeOhlc() { } catch (Exception e) { log.error("△ 실시간 OHLC 데이터 발행 중 오류: {}", e.getMessage(), e); } - } - - } \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/controller/MinuteOhlcDataController.java b/src/main/java/com/cleanengine/coin/chart/controller/MinuteOhlcDataController.java index 6650f373..0028fd30 100644 --- a/src/main/java/com/cleanengine/coin/chart/controller/MinuteOhlcDataController.java +++ b/src/main/java/com/cleanengine/coin/chart/controller/MinuteOhlcDataController.java @@ -1,11 +1,12 @@ package com.cleanengine.coin.chart.controller; import com.cleanengine.coin.chart.dto.RealTimeOhlcDto; -import com.cleanengine.coin.chart.service.minute.MinuteOhlcDataService; +import com.cleanengine.coin.chart.service.minute.PagingMinuteOhlcDataService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; import java.util.List; @RestController @@ -13,17 +14,25 @@ @RequiredArgsConstructor public class MinuteOhlcDataController { - private final MinuteOhlcDataService service; + private final PagingMinuteOhlcDataService service; /** - * GET /api/minute-ohlc?ticker=BTC - * DB에 있는 과거 거래를 1분 단위로 묶어 OHLC+volume을 계산한 리스트 반환 + * GET /api/minute-ohlc?ticker=BTC&count=100&interval=1&from=2025-06-19T10:30 + * DB에 있는 과거 거래를 interval 단위로 묶어 OHLC+volume을 계산한 리스트 반환 */ @GetMapping public ResponseEntity> getMinuteOhlc( - @RequestParam("ticker") String ticker + @RequestParam("ticker") String ticker, + @RequestParam(value = "count", defaultValue = "100") int count, + @RequestParam(value = "interval", defaultValue = "1") int interval, + @RequestParam(value = "from", required = false) LocalDateTime from ) { - List data = service.getMinuteOhlcData(ticker); + if (from == null) { + from = LocalDateTime.now(); + } + + List data = service.getMinuteOhlcData(ticker, count, interval, from.minusMinutes(1)); return ResponseEntity.ok(data); } + } \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java b/src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java index be07435d..f77af8e6 100644 --- a/src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java +++ b/src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java @@ -2,7 +2,7 @@ import com.cleanengine.coin.chart.dto.RealTimeOhlcDto; import com.cleanengine.coin.chart.service.ChartSubscriptionService; -import com.cleanengine.coin.chart.service.RealTimeOhlcService; +import com.cleanengine.coin.chart.service.RealTimeOhlcService; // RealTimeOhlcService 의존성은 이제 불필요 import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; @@ -19,9 +19,8 @@ public class WebSocketMessageController { private final ChartSubscriptionService subscriptionService; - private final RealTimeOhlcService realTimeOhlcService; private final SimpMessagingTemplate messagingTemplate; - private final ChartDataController chartDataController; + private final ChartDataController chartDataController; // 이미 계산된 데이터를 가진 컨트롤러를 활용 /** * 실시간 OHLC 데이터 구독 처리 @@ -34,30 +33,16 @@ public void subscribeRealTimeOhlc(RealTimeTradeMappingDto request) { // 구독 목록에 추가 subscriptionService.subscribeRealTimeOhlc(ticker); - // 구독 즉시 최근 실시간 OHLC 데이터 전송 - RealTimeOhlcDto latestOhlcData = realTimeOhlcService.getRealTimeOhlc(ticker); - RealTimeOhlcDto lastSentData = chartDataController.getLastSentOhlcDataMap().get(ticker); - if (latestOhlcData == null) { - if (lastSentData != null) { - // 이전에 전송한 데이터가 있으면 재사용 - log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 이전 데이터 재사용", ticker); - messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, lastSentData); - } else { - // 이전 데이터도 없는 경우 빈 데이터 전송 - log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 빈 데이터 전송", ticker); - RealTimeOhlcDto emptyData = createEmptyRealTimeOhlcDto(ticker); - messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData); - // 빈 데이터도 캐시에 저장 - chartDataController.getLastSentOhlcDataMap().put(ticker, emptyData); - } + if (lastSentData == null) { + log.debug("티커 {}의 캐시된 OHLC 데이터가 없습니다. 빈 데이터 전송", ticker); + RealTimeOhlcDto emptyData = createEmptyRealTimeOhlcDto(ticker); + messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData); } else { - log.debug("티커 {}의 실시간 OHLC 데이터 전송: {}", ticker, latestOhlcData); - messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, latestOhlcData); - // 데이터 캐시에 저장 - chartDataController.getLastSentOhlcDataMap().put(ticker, latestOhlcData); - + // 캐시된 데이터가 있으면 즉시 전송 + log.debug("티커 {}의 캐시된 OHLC 데이터 즉시 전송: {}", ticker, lastSentData); + messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, lastSentData); } } @@ -80,6 +65,5 @@ private RealTimeOhlcDto createEmptyRealTimeOhlcDto(String ticker) { @Getter public static class RealTimeTradeMappingDto { private String ticker; - } } \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java b/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java index db294c80..656829be 100644 --- a/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java +++ b/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java @@ -23,6 +23,7 @@ public class TradeEventHandler { private final WebsocketSendService websocketSendService; private final ChartSubscriptionService chartSubscriptionService; // 주입 private final RealTimeDataPrevRateService realTimeDataPrevRateService; + //event로 이벤틀 처리해야한다. //eventListener는 void로 처리를 해야한다 @TransactionalEventListener @@ -37,8 +38,29 @@ public void handleTradeEvent(TradeExecutedEvent event) { TradeEventDto tradeEventDto = getTradeEventDto(trade); log.debug("TradeEventHandler 수신 : {}", trade); - // 실시간 데이터 체결내역 - // 해당 종목에 대한 구독자가 있는지 확인 + // 실시간 데이터 전송 + processRealTimeTradeRate(ticker, tradeEventDto); + + //전날 종가 변동률 전송 + processPrevRateData(ticker, tradeEventDto); + } + + public void processPrevRateData(String ticker, TradeEventDto tradeEventDto) { + if (chartSubscriptionService.isSubscribedToPrevRate(ticker)) { + log.debug("종목 {} 전일 대비 변동률 구독자 확인됨. 데이터 전송 시작.", ticker); + try { + PrevRateDto dto = realTimeDataPrevRateService.generatePrevRateData(tradeEventDto); + websocketSendService.sendPrevRate(dto, dto.getTicker()); // dto.getTicker()는 이미 ticker와 동일 + log.debug("종목 {} 전일 대비 변동률 전송 완료 : {}", ticker, dto); + } catch (Exception e) { + log.error("종목 {} 전일 대비 변동률 전송 중 오류: {}", ticker, e.getMessage(), e); + } + } else { + log.debug("종목 {} 전일 대비 변동률 구독자 없음. 데이터 전송 생략.", ticker); + } + } + + public void processRealTimeTradeRate(String ticker, TradeEventDto tradeEventDto) { if (chartSubscriptionService.isSubscribedToRealTimeTradeRate(ticker)) { log.debug("종목 {} 실시간 체결 정보 구독자 확인됨. 데이터 처리 및 전송 시작.", ticker); @@ -54,21 +76,6 @@ public void handleTradeEvent(TradeExecutedEvent event) { } else { log.debug("종목 {} 실시간 체결 정보 구독자 없음. 데이터 전송 생략.", ticker); } - - //전날 종가 변동률 전송 - if(chartSubscriptionService.isSubscribedToPrevRate(ticker)) { - log.debug("종목 {} 전일 대비 변동률 구독자 확인됨. 데이터 전송 시작.", ticker); - try { - PrevRateDto dto = realTimeDataPrevRateService.generatePrevRateData(tradeEventDto); - websocketSendService.sendPrevRate(dto, dto.getTicker()); // dto.getTicker()는 이미 ticker와 동일 - log.debug("종목 {} 전일 대비 변동률 전송 완료 : {}", ticker, dto); - } catch (Exception e) { - log.error("종목 {} 전일 대비 변동률 전송 중 오류: {}", ticker, e.getMessage(), e); - } - }else { - log.debug("종목 {} 전일 대비 변동률 구독자 없음. 데이터 전송 생략.", ticker); - } - } @NotNull diff --git a/src/main/java/com/cleanengine/coin/chart/repository/ChartDataRepository.java b/src/main/java/com/cleanengine/coin/chart/repository/ChartDataRepository.java deleted file mode 100644 index bc393874..00000000 --- a/src/main/java/com/cleanengine/coin/chart/repository/ChartDataRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.cleanengine.coin.chart.repository; - -import java.time.LocalDateTime; -import java.util.List; - -import org.springframework.data.repository.query.Param; - -public interface ChartDataRepository { - /** - * Projection interface for minute-candle aggregation results. - */ - interface MinuteCandleProjection { - String getTicker(); - LocalDateTime getBucketStart(); - Double getLowPrice(); - Double getHighPrice(); - Double getOpenPrice(); - Double getClosePrice(); - Double getVolume(); - } - - /** - 시작시간과 끝시간 (1분)의 ohlc 데이터를 반환 - */ - List findMinuteCandles( - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end); - - /** - * 특정 티커의 캔들 데이터만 조회 - */ - List findMinuteCandlesByTicker( - @Param("ticker") String ticker, - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end); -} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/repository/H2ChartDataRepository.java b/src/main/java/com/cleanengine/coin/chart/repository/H2ChartDataRepository.java deleted file mode 100644 index d7b63cb7..00000000 --- a/src/main/java/com/cleanengine/coin/chart/repository/H2ChartDataRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.cleanengine.coin.chart.repository; - -import com.cleanengine.coin.trade.entity.Trade; -import org.springframework.context.annotation.Profile; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.time.LocalDateTime; -import java.util.List; - -@Repository -@Profile({"h2", "default"}) // 기본 프로필과 h2 프로필에서 사용 -public interface H2ChartDataRepository extends ChartDataRepository, JpaRepository { - - @Override - @Query(value = """ - SELECT - t.ticker AS ticker, - PARSEDATETIME(FORMATDATETIME(t.trade_time, 'yyyy-MM-dd HH:mm:00'), 'yyyy-MM-dd HH:mm:ss') AS bucket_start, - MIN(t.price) AS low_price, - MAX(t.price) AS high_price, - AVG(t.price) AS open_price, -- 간소화: 개/종가 대신 평균가 사용 - AVG(t.price) AS close_price, -- 간소화: 개/종가 대신 평균가 사용 - SUM(t.size) AS volume - FROM trade t - WHERE t.trade_time >= :start AND t.trade_time < :end - GROUP BY t.ticker, PARSEDATETIME(FORMATDATETIME(t.trade_time, 'yyyy-MM-dd HH:mm:00'), 'yyyy-MM-dd HH:mm:ss') - ORDER BY t.ticker, bucket_start -""", nativeQuery = true) - List findMinuteCandles( - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end); - - @Override - @Query(value = """ - SELECT - t.ticker AS ticker, - PARSEDATETIME(FORMATDATETIME(t.trade_time, 'yyyy-MM-dd HH:mm:00'), 'yyyy-MM-dd HH:mm:ss') AS bucket_start, - MIN(t.price) AS low_price, - MAX(t.price) AS high_price, - AVG(t.price) AS open_price, -- 간소화: 개/종가 대신 평균가 사용 - AVG(t.price) AS close_price, -- 간소화: 개/종가 대신 평균가 사용 - SUM(t.size) AS volume - FROM trade t - WHERE UPPER(t.ticker) = UPPER(:ticker) AND t.trade_time >= :start AND t.trade_time < :end - GROUP BY t.ticker, PARSEDATETIME(FORMATDATETIME(t.trade_time, 'yyyy-MM-dd HH:mm:00'), 'yyyy-MM-dd HH:mm:ss') - ORDER BY bucket_start -""", nativeQuery = true) - List findMinuteCandlesByTicker( - @Param("ticker") String ticker, - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end); -} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/repository/MariaDbChartDataRepository.java b/src/main/java/com/cleanengine/coin/chart/repository/MariaDbChartDataRepository.java deleted file mode 100644 index 5bd0271d..00000000 --- a/src/main/java/com/cleanengine/coin/chart/repository/MariaDbChartDataRepository.java +++ /dev/null @@ -1,94 +0,0 @@ -//package com.cleanengine.coin.chart.repository; -// -//import com.cleanengine.coin.trade.entity.Trade; -//import org.springframework.context.annotation.Profile; -//import org.springframework.data.jpa.repository.JpaRepository; -//import org.springframework.data.jpa.repository.Query; -//import org.springframework.data.repository.query.Param; -//import org.springframework.stereotype.Repository; -// -//import java.time.LocalDateTime; -//import java.util.List; -// -//@Repository -//@Profile("mariadb") // mariadb 프로필에서 사용 -//public interface MariaDbChartDataRepository extends ChartDataRepository, JpaRepository { -// -// @Override -// @Query(value = """ -// WITH cte AS ( -// SELECT -// t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) AS bucket_start, -// t.price, -// t.size, -// ROW_NUMBER() OVER ( -// PARTITION BY t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) -// ORDER BY t.trade_time ASC -// ) AS rn_open, -// ROW_NUMBER() OVER ( -// PARTITION BY t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) -// ORDER BY t.trade_time DESC -// ) AS rn_close -// FROM trade t -// WHERE t.trade_time >= :start -// AND t.trade_time < :end -// ) -// SELECT -// ticker, -// bucket_start, -// MAX(CASE WHEN rn_open = 1 THEN price END) AS open_price, -// MAX(price) AS high_price, -// MIN(price) AS low_price, -// MAX(CASE WHEN rn_close = 1 THEN price END) AS close_price, -// SUM(size) AS volume -// FROM cte -// GROUP BY ticker, bucket_start -// ORDER BY ticker, bucket_start -// """, nativeQuery = true) -// List findMinuteCandles( -// @Param("start") LocalDateTime start, -// @Param("end") LocalDateTime end); -// -// @Override -// @Query(value = """ -// WITH cte AS ( -// SELECT -// t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) AS bucket_start, -// t.price, -// t.size, -// ROW_NUMBER() OVER ( -// PARTITION BY t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) -// ORDER BY t.trade_time ASC -// ) AS rn_open, -// ROW_NUMBER() OVER ( -// PARTITION BY t.ticker, -// TIMESTAMP(DATE_FORMAT(t.trade_time, '%%Y-%%m-%%d %%H:%%i:00')) -// ORDER BY t.trade_time DESC -// ) AS rn_close -// FROM trade t -// WHERE UPPER(t.ticker) = UPPER(:ticker) -// AND t.trade_time >= :start -// AND t.trade_time < :end -// ) -// SELECT -// ticker, -// bucket_start, -// MAX(CASE WHEN rn_open = 1 THEN price END) AS open_price, -// MAX(price) AS high_price, -// MIN(price) AS low_price, -// MAX(CASE WHEN rn_close = 1 THEN price END) AS close_price, -// SUM(size) AS volume -// FROM cte -// GROUP BY ticker, bucket_start -// ORDER BY bucket_start -// """, nativeQuery = true) -// List findMinuteCandlesByTicker( -// @Param("ticker") String ticker, -// @Param("start") LocalDateTime start, -// @Param("end") LocalDateTime end); -//} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java b/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java index 26d3f177..58ca786a 100644 --- a/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java +++ b/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java @@ -45,9 +45,7 @@ public Set getAllRealTimeTradeRateSubscribedTickers() { //종목에 대한 구독 여부 public boolean isSubscribedToRealTimeTradeRate(String ticker) { - if (ticker == null || ticker.trim().isEmpty()) { - return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리 - } + validateTicker(ticker); return realTimeTradeRateSubscribedTickers.contains(ticker); } @@ -76,9 +74,7 @@ public Set getAllRealTimeOhlcSubscribedTickers() { } public boolean isSubscribedToRealTimeOhlc(String ticker) { - if (ticker == null || ticker.trim().isEmpty()) { - return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리 - } + validateTicker(ticker); return realTimeOhlcSubscribedTickers.contains(ticker); } @@ -103,9 +99,7 @@ public Set getAllPrevRateSubscribedTickers(String ticker) { } public boolean isSubscribedToPrevRate(String ticker) { - if (ticker == null || ticker.trim().isEmpty()) { - return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리 - } + validateTicker(ticker); return PrevRateSubscribedTickers.contains(ticker); } diff --git a/src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java b/src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java index 19298eb1..4e45b1ae 100644 --- a/src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java +++ b/src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -20,101 +21,86 @@ public class RealTimeOhlcService { private final TradeRepository tradeRepository; - // 티커별 마지막 처리 시간 - private final Map lastProcessedTimeMap = new ConcurrentHashMap<>(); - // 티커별 마지막 OHLC 데이터 캐싱 - private final Map lastOhlcDataMap = new ConcurrentHashMap<>(); + private final Map currentMinuteOhlcCache = new ConcurrentHashMap<>(); - /** - * 특정 티커의 최신 1초 OHLC 데이터 생성 - */ - public RealTimeOhlcDto getRealTimeOhlc(String ticker) { - try { - LocalDateTime now = LocalDateTime.now(); - // 시간 범위 계산 - TimeRange timeRange = calculateTimeRange(ticker, now); + public RealTimeOhlcDto getAndUpdateCumulative1mOhlc(String ticker, LocalDateTime now ) { + try { + LocalDateTime currentMinuteStart = now.truncatedTo(ChronoUnit.MINUTES); - // 거래 데이터 조회 및 전처리 - List recentTrades = getProcessedTradeData(ticker, timeRange); + RealTimeOhlcDto cachedOhlc = currentMinuteOhlcCache.get(ticker); - // 거래 데이터가 없으면 캐시된 데이터 반환 - if (recentTrades.isEmpty()) { - return getCachedData(ticker); + if (cachedOhlc == null || cachedOhlc.getTimestamp().isBefore(currentMinuteStart)) { + return handleNewMinute(ticker, now, currentMinuteStart); } + else { + return handleExistingMinute(ticker, now, cachedOhlc); + } + } catch (Exception e) { + log.error("티커 {}의 누적 OHLC 데이터 생성 중 오류 발생: {}", ticker, e.getMessage(), e); + // 오류 발생 시 캐시된 마지막 데이터라도 반환 + return currentMinuteOhlcCache.get(ticker); + } + } - calculateOhlcv ohlcv = getCalculateOhlcv(recentTrades); + private RealTimeOhlcDto handleNewMinute(String ticker, LocalDateTime now, LocalDateTime minuteStart) { + log.debug("티커 {}: 새로운 1분봉 시작 ({}).", ticker, minuteStart); + List trades = tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(ticker, minuteStart, now); - RealTimeOhlcDto ohlcData = createOhlcDto(ticker, now, ohlcv); + if (trades.isEmpty()) { + return null; + } - // 캐시 업데이트 - updateCache(ticker, now, ohlcData); + // 새 거래내역으로 OHLCV 계산 + CalculateOhlcv ohlcv = getCalculateOhlcv(trades); + RealTimeOhlcDto newOhlc = createOhlcDto(ticker, now, ohlcv); - return ohlcData; - } catch (Exception e) { - log.error("실시간 OHLC 데이터 생성 중 오류: {}", e.getMessage(), e); - return getCachedData(ticker); - } + // 캐시를 새로운 1분봉 데이터로 교체 + currentMinuteOhlcCache.put(ticker, newOhlc); + return newOhlc; } - // 시간 범위 계산 - TimeRange calculateTimeRange(String ticker, LocalDateTime now) { - LocalDateTime lastProcessedTime = lastProcessedTimeMap.getOrDefault( - ticker, now.minusSeconds(1)); - return new TimeRange(lastProcessedTime, now); - } - // 거래 데이터 조회 및 전처리 - List getProcessedTradeData(String ticker, TimeRange timeRange) { - return tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc( - ticker, - timeRange.start(), - timeRange.end() - ); - } + private RealTimeOhlcDto handleExistingMinute(String ticker, LocalDateTime now, RealTimeOhlcDto cachedOhlc) { + LocalDateTime lastProcessedTime = cachedOhlc.getTimestamp(); + List newTrades = tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(ticker, lastProcessedTime, now); - // 캐시 업데이트 - void updateCache(String ticker, LocalDateTime now, RealTimeOhlcDto ohlcData) { - lastProcessedTimeMap.put(ticker, now); - lastOhlcDataMap.put(ticker, ohlcData); - } + // 새로운 거래가 없다면, 타임스탬프만 최신으로 업데이트하여 "살아있음"을 알림 + if (newTrades.isEmpty()) { + cachedOhlc.setTimestamp(now); + return cachedOhlc; + } - // 캐시된 데이터 조회 - RealTimeOhlcDto getCachedData(String ticker) { - return lastOhlcDataMap.getOrDefault(ticker, null); - } + log.trace("티커 {}: 기존 1분봉 업데이트. 신규 거래 {}건", ticker, newTrades.size()); - // DTO 생성 - RealTimeOhlcDto createOhlcDto(String ticker, LocalDateTime timestamp, calculateOhlcv ohlcv) { - return new RealTimeOhlcDto( - ticker, - timestamp, - ohlcv.open(), - ohlcv.high(), - ohlcv.low(), - ohlcv.close(), - ohlcv.volume() - ); + // Open(시가)는 분이 끝날때까지 고정 + cachedOhlc.setHigh(Math.max(cachedOhlc.getHigh(), newTrades.stream().mapToDouble(Trade::getPrice).max().orElse(cachedOhlc.getHigh()))); + cachedOhlc.setLow(Math.min(cachedOhlc.getLow(), newTrades.stream().mapToDouble(Trade::getPrice).min().orElse(cachedOhlc.getLow()))); + cachedOhlc.setClose(newTrades.getLast().getPrice()); // 종가는 항상 마지막 거래 가격 + cachedOhlc.setVolume(cachedOhlc.getVolume() + newTrades.stream().mapToDouble(Trade::getSize).sum()); + cachedOhlc.setTimestamp(now); // 마지막 처리 시간 갱신 + + return cachedOhlc; } - // OHLCV 계산 메서드 + + @NotNull - static calculateOhlcv getCalculateOhlcv(List trades) { - // trades는 시간 오름차순 정렬되어 있음 - Double open = trades.getFirst().getPrice(); // 첫 번째(가장 오래된) = Open ✅ - Double close = trades.getLast().getPrice(); // 마지막(가장 최근) = Close ✅ + private CalculateOhlcv getCalculateOhlcv(List trades) { + Double open = trades.getFirst().getPrice(); + Double close = trades.getLast().getPrice(); Double high = trades.stream().mapToDouble(Trade::getPrice).max().orElse(0.0); Double low = trades.stream().mapToDouble(Trade::getPrice).min().orElse(0.0); Double volume = trades.stream().mapToDouble(Trade::getSize).sum(); - - return new calculateOhlcv(open, high, low, close, volume); + return new CalculateOhlcv(open, high, low, close, volume); } + private RealTimeOhlcDto createOhlcDto(String ticker, LocalDateTime timestamp, CalculateOhlcv ohlcv) { + return new RealTimeOhlcDto( + ticker, timestamp, ohlcv.open(), ohlcv.high(), ohlcv.low(), ohlcv.close(), ohlcv.volume() + ); + } - - - record TimeRange(LocalDateTime start, LocalDateTime end) {} - - record calculateOhlcv(Double open, Double high, Double low, Double close, Double volume) {} + record CalculateOhlcv(Double open, Double high, Double low, Double close, Double volume) {} } \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java b/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java index 5d1d20e6..26d36bfb 100644 --- a/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java +++ b/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java @@ -94,8 +94,8 @@ void validateTradeList(List trades) { // OHLC 계산 로직 @NotNull static OhlcData calculateOhlcData(List trades) { - double open = trades.get(0).getPrice(); - double close = trades.get(trades.size() - 1).getPrice(); + double open = trades.getFirst().getPrice(); + double close = trades.getLast().getPrice(); double high = trades.stream() .mapToDouble(Trade::getPrice) diff --git a/src/main/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataService.java b/src/main/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataService.java new file mode 100644 index 00000000..f8bffd19 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataService.java @@ -0,0 +1,12 @@ +package com.cleanengine.coin.chart.service.minute; + +import com.cleanengine.coin.chart.dto.RealTimeOhlcDto; + +import java.time.LocalDateTime; +import java.util.List; + +public interface PagingMinuteOhlcDataService { + + List getMinuteOhlcData(String ticker, int count, int interval, LocalDateTime from); + +} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataServiceImpl.java b/src/main/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataServiceImpl.java new file mode 100644 index 00000000..a6befcd9 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataServiceImpl.java @@ -0,0 +1,115 @@ +package com.cleanengine.coin.chart.service.minute; + +import com.cleanengine.coin.chart.dto.RealTimeOhlcDto; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class PagingMinuteOhlcDataServiceImpl implements PagingMinuteOhlcDataService { + + private final EntityManager em; + + @Override + public List getMinuteOhlcData(String ticker, int count, int interval, LocalDateTime from) { + validateTicker(ticker); + + String timeSlotQuery = """ + SELECT DISTINCT DATE_FORMAT( + DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE), + '%Y-%m-%d %H:%i:00') AS time_slot + FROM trade + WHERE ticker = :ticker AND trade_time <= :from + GROUP BY DATE_FORMAT( + DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE), + '%Y-%m-%d %H:%i:00') + ORDER BY time_slot DESC + LIMIT :count + """; + + Query nativeQuery = em.createNativeQuery(timeSlotQuery); + nativeQuery.setParameter("ticker", ticker); + nativeQuery.setParameter("from", from); + nativeQuery.setParameter("interval", interval); + nativeQuery.setParameter("count", count); + + @SuppressWarnings("unchecked") + List timeSlotResults = nativeQuery.getResultList(); + List timeSlots = timeSlotResults.stream() + .map(str -> LocalDateTime.parse(str, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) + .sorted(Comparator.naturalOrder()) + .toList(); + + if (timeSlots.isEmpty()) { + return Collections.emptyList(); + } + + String ohlcQuery = """ + SELECT + :ticker AS ticker, + t.time_slot AS timestamp, + MAX(CASE WHEN rn_open = 1 THEN price END) AS open, + MAX(price) AS high, + MIN(price) AS low, + MAX(CASE WHEN rn_close = 1 THEN price END) AS close, + SUM(size) AS volume + FROM ( + SELECT + trade_time, + price, + size, + DATE_FORMAT( + DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE), + '%Y-%m-%d %H:%i:00') AS time_slot, + ROW_NUMBER() OVER (PARTITION BY DATE_FORMAT( + DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE), + '%Y-%m-%d %H:%i:00') ORDER BY trade_time) AS rn_open, + ROW_NUMBER() OVER (PARTITION BY DATE_FORMAT( + DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE), + '%Y-%m-%d %H:%i:00') ORDER BY trade_time DESC) AS rn_close + FROM trade + WHERE ticker = :ticker + AND DATE_FORMAT( + DATE_SUB(trade_time, INTERVAL MOD(MINUTE(trade_time), :interval) MINUTE), + '%Y-%m-%d %H:%i:00') IN (:timeSlots) + ) t + GROUP BY t.time_slot + ORDER BY t.time_slot + """; + + Query ohlcNativeQuery = em.createNativeQuery(ohlcQuery); + ohlcNativeQuery.setParameter("ticker", ticker); + ohlcNativeQuery.setParameter("interval", interval); + ohlcNativeQuery.setParameter("timeSlots", timeSlotResults); + + @SuppressWarnings("unchecked") + List results = ohlcNativeQuery.getResultList(); + return results.stream() + .map(row -> new RealTimeOhlcDto( + (String) row[0], // ticker + LocalDateTime.parse((String) row[1], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + ((Number) row[2]).doubleValue(), // open + ((Number) row[3]).doubleValue(), // high + ((Number) row[4]).doubleValue(), // low + ((Number) row[5]).doubleValue(), // close + ((Number) row[6]).doubleValue() // volume + )) + .collect(Collectors.toList()); + } + + static void validateTicker(String ticker) { + if (ticker == null || ticker.trim().isEmpty()) { + throw new IllegalArgumentException("티커는 비어있을 수 없습니다"); + } + } + +} diff --git a/src/main/java/com/cleanengine/coin/configuration/MicrometerConfig.java b/src/main/java/com/cleanengine/coin/configuration/MicrometerConfig.java new file mode 100644 index 00000000..68cd9103 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/configuration/MicrometerConfig.java @@ -0,0 +1,32 @@ +package com.cleanengine.coin.configuration; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.config.MeterFilterReply; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Profile("actuator") +@Configuration +public class MicrometerConfig { + @Bean + public MeterFilter meterFilter() { + final String hibernateFilterPrefix = "hibernate.statements"; + + return new MeterFilter() { + @Override + public MeterFilterReply accept(Meter.Id id) { + if (id.getName().startsWith("hibernate.")) { + if(id.getName().startsWith(hibernateFilterPrefix)) { + return MeterFilterReply.ACCEPT; + } + else{ + return MeterFilterReply.DENY; + } + } + return MeterFilterReply.NEUTRAL; + } + }; + } +} diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java index 83d29bf6..3530d960 100644 --- a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java @@ -9,7 +9,7 @@ import java.util.Optional; @Slf4j -@Component +//@Component public class InMemoryWaitingOrdersManager implements WaitingOrdersManager { private final HashMap waitingOrdersMap = new HashMap<>(); @@ -29,7 +29,7 @@ public WaitingOrders getWaitingOrders(String ticker) { @Override public void removeWaitingOrders(String ticker) { - + waitingOrdersMap.remove(ticker); } @Override diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryBuyOrderSkipListSet.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryBuyOrderSkipListSet.java new file mode 100644 index 00000000..ab35bef3 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryBuyOrderSkipListSet.java @@ -0,0 +1,96 @@ +package com.cleanengine.coin.order.adapter.out.persistentce.order.queue; + +import com.cleanengine.coin.common.domain.port.PriorityQueueStore; +import com.cleanengine.coin.order.domain.BuyOrder; + +import java.util.Comparator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLong; + +public class NestedInMemoryBuyOrderSkipListSet implements PriorityQueueStore { + private final ConcurrentSkipListMap> map = new ConcurrentSkipListMap<>(Comparator.reverseOrder()); + private final AtomicLong size = new AtomicLong(); + + @Override + public void put(BuyOrder item) { + if(item == null) throw new IllegalArgumentException("item cannot be null."); + + map.compute(item.getPrice(), (key, buyOrders) -> { + if(buyOrders == null) { + buyOrders = new ConcurrentSkipListSet<>(); + } + boolean added = buyOrders.add(item); + if(added) size.incrementAndGet(); + return buyOrders; + }); + } + + @Override + public BuyOrder poll() { + while(true) { + Map.Entry> firstEntry = map.firstEntry(); + + if (firstEntry == null) { + return null; + } + + ConcurrentSkipListSet buyOrders = firstEntry.getValue(); + try { + BuyOrder order = buyOrders.first(); + this.remove(order); + return order; + } catch (NoSuchElementException e) { + continue; + } + } + } + + @Override + public BuyOrder peek() { + while(true) { + Map.Entry> firstEntry = map.firstEntry(); + + if (firstEntry == null) { + return null; + } + + ConcurrentSkipListSet buyOrders = firstEntry.getValue(); + try { + return buyOrders.first(); + } catch (NoSuchElementException e) { + continue; + } + } + } + + @Override + public BuyOrder remove(BuyOrder item) { + if (item == null) return null; + + map.computeIfPresent(item.getPrice(), (key, orders) -> { + boolean removed = orders.remove(item); + if (removed) size.decrementAndGet(); + return orders.isEmpty() ? null : orders; + }); + + return item; + } + + @Override + public long size() { + return size.get(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public void clear() { + map.clear(); + } +} diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemorySellOrderSkipListSet.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemorySellOrderSkipListSet.java new file mode 100644 index 00000000..29cd1368 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemorySellOrderSkipListSet.java @@ -0,0 +1,97 @@ +package com.cleanengine.coin.order.adapter.out.persistentce.order.queue; + +import com.cleanengine.coin.common.domain.port.PriorityQueueStore; +import com.cleanengine.coin.order.domain.BuyOrder; +import com.cleanengine.coin.order.domain.SellOrder; + +import java.util.Comparator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLong; + +public class NestedInMemorySellOrderSkipListSet implements PriorityQueueStore { + private final ConcurrentSkipListMap> map = new ConcurrentSkipListMap<>(); + private final AtomicLong size = new AtomicLong(); + + @Override + public void put(SellOrder item) { + if(item == null) throw new IllegalArgumentException("item cannot be null."); + + map.compute(item.getPrice(), (key, sellOrders) -> { + if(sellOrders == null) { + sellOrders = new ConcurrentSkipListSet<>(); + } + boolean added = sellOrders.add(item); + if(added) size.incrementAndGet(); + return sellOrders; + }); + } + + @Override + public SellOrder poll() { + while(true) { + Map.Entry> firstEntry = map.firstEntry(); + + if (firstEntry == null) { + return null; + } + + ConcurrentSkipListSet sellOrders = firstEntry.getValue(); + try { + SellOrder order = sellOrders.first(); + this.remove(order); + return order; + } catch (NoSuchElementException e) { + continue; + } + } + } + + @Override + public SellOrder peek() { + while(true) { + Map.Entry> firstEntry = map.firstEntry(); + + if (firstEntry == null) { + return null; + } + + ConcurrentSkipListSet sellOrders = firstEntry.getValue(); + try { + return sellOrders.first(); + } catch (NoSuchElementException e) { + continue; + } + } + } + + @Override + public SellOrder remove(SellOrder item) { + if (item == null) return null; + + map.computeIfPresent(item.getPrice(), (key, orders) -> { + boolean removed = orders.remove(item); + if (removed) size.decrementAndGet(); + return orders.isEmpty() ? null : orders; + }); + + return item; + } + + @Override + public long size() { + return size.get(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public void clear() { + map.clear(); + } +} diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrders.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrders.java new file mode 100644 index 00000000..73c44c11 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrders.java @@ -0,0 +1,89 @@ +package com.cleanengine.coin.order.adapter.out.persistentce.order.queue; + +import com.cleanengine.coin.common.adapter.out.store.InMemoryPriorityQueueStore; +import com.cleanengine.coin.common.domain.port.PriorityQueueStore; +import com.cleanengine.coin.order.domain.BuyOrder; +import com.cleanengine.coin.order.domain.Order; +import com.cleanengine.coin.order.domain.OrderType; +import com.cleanengine.coin.order.domain.SellOrder; +import com.cleanengine.coin.order.domain.spi.WaitingOrders; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class NestedInMemoryWaitingOrders implements WaitingOrders { + private final String ticker; + private final PriorityQueueStore limitBuyOrderPriorityQueueStore = new NestedInMemoryBuyOrderSkipListSet(); + private final PriorityQueueStore marketBuyOrderPriorityQueueStore = new InMemoryPriorityQueueStore<>(); + private final PriorityQueueStore limitSellOrderPriorityQueueStore = new NestedInMemorySellOrderSkipListSet(); + private final PriorityQueueStore marketSellOrderPriorityQueueStore = new InMemoryPriorityQueueStore<>(); + + @Override + public String getTicker() { + return ticker; + } + + @Override + public void addOrder(Order order) { + if(order == null) throw new IllegalArgumentException("order cannot be null."); + + if (order instanceof BuyOrder) { + if(order.getIsMarketOrder()) { + marketBuyOrderPriorityQueueStore.put((BuyOrder) order); + } + else{ + limitBuyOrderPriorityQueueStore.put((BuyOrder) order); + } + } else { + if(order.getIsMarketOrder()) { + marketSellOrderPriorityQueueStore.put((SellOrder) order); + } + else{ + limitSellOrderPriorityQueueStore.put((SellOrder) order); + } + } + } + + @Override + public PriorityQueueStore getBuyOrderPriorityQueueStore(OrderType orderType) { + return orderType == OrderType.MARKET ? marketBuyOrderPriorityQueueStore : limitBuyOrderPriorityQueueStore; + } + + @Override + public PriorityQueueStore getSellOrderPriorityQueueStore(OrderType orderType) { + return orderType == OrderType.MARKET ? marketSellOrderPriorityQueueStore : limitSellOrderPriorityQueueStore; + } + + @Override + public void removeOrder(Order order) { + if(order == null) throw new IllegalArgumentException("order cannot be null."); + + if (order instanceof BuyOrder) { + if(order.getIsMarketOrder()) { + marketBuyOrderPriorityQueueStore.remove((BuyOrder) order); + } + else{ + limitBuyOrderPriorityQueueStore.remove((BuyOrder) order); + } + } else { + if(order.getIsMarketOrder()) { + marketSellOrderPriorityQueueStore.remove((SellOrder) order); + } + else{ + limitSellOrderPriorityQueueStore.remove((SellOrder) order); + } + } + } + + @Override + public void clearAllQueues() { + limitBuyOrderPriorityQueueStore.clear(); + marketBuyOrderPriorityQueueStore.clear(); + limitSellOrderPriorityQueueStore.clear(); + marketSellOrderPriorityQueueStore.clear(); + } + + @Override + public void close() { + + } +} diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrdersManager.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrdersManager.java new file mode 100644 index 00000000..0fd9b21e --- /dev/null +++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/NestedInMemoryWaitingOrdersManager.java @@ -0,0 +1,44 @@ +package com.cleanengine.coin.order.adapter.out.persistentce.order.queue; + +import com.cleanengine.coin.order.domain.spi.WaitingOrders; +import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Optional; + +@Slf4j +@Component +public class NestedInMemoryWaitingOrdersManager implements WaitingOrdersManager { + private final HashMap waitingOrdersMap = new HashMap<>(); + + @Override + public WaitingOrders getWaitingOrders(String ticker) { + if(!waitingOrdersMap.containsKey(ticker)) { + addWaitingOrders(ticker); + } + + Optional waitingOrdersOpt = Optional.ofNullable(waitingOrdersMap.get(ticker)); + if(waitingOrdersOpt.isEmpty()){ + log.debug("WaitingOrders not found. with " + ticker); + throw new RuntimeException("WaitingOrders not found with " + ticker); + } + return waitingOrdersOpt.get(); } + + @Override + public void removeWaitingOrders(String ticker) { + waitingOrdersMap.remove(ticker); + } + + @Override + public void close() { + + } + + protected synchronized void addWaitingOrders(String ticker){ + if(!waitingOrdersMap.containsKey(ticker)){ + waitingOrdersMap.put(ticker, new NestedInMemoryWaitingOrders(ticker)); + } + } +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java index 9bf89e12..396f7b90 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -1,6 +1,5 @@ package com.cleanengine.coin.realitybot.api; -import com.cleanengine.coin.common.annotation.WorkingServerProfile; import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; import com.cleanengine.coin.order.domain.Asset; import com.cleanengine.coin.realitybot.domain.APIVWAPState; @@ -8,6 +7,7 @@ import com.cleanengine.coin.realitybot.dto.Ticks; import com.cleanengine.coin.realitybot.parser.TickParser; import com.cleanengine.coin.realitybot.service.OrderGenerateService; +import com.cleanengine.coin.realitybot.service.PlatformVWAPService; import com.cleanengine.coin.realitybot.service.TickServiceManager; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; @@ -33,9 +33,9 @@ public class ApiScheduler { private final CoinoneAPIClient coinoneAPIClient; private final VWAPMetricsRecorder recorder; private final MeterRegistry meterRegistry; - private String ticker; + private final PlatformVWAPService platformVWAPService; -// @Scheduled(fixedRate = 5000) + // @Scheduled(fixedRate = 5000) public void MarketAllRequest() throws InterruptedException { Timer timer = meterRegistry.timer("apischeduler.request.duration"); timer.record(() -> { @@ -48,13 +48,13 @@ public void MarketAllRequest() throws InterruptedException { } public void MarketDataRequest(String ticker){ - this.ticker = ticker; String rawJson = bithumbAPIClient.get(ticker); //api raw데이터 // String rawJson = getMarketDataWithFallback(ticker); List gson = tickParser.parseGson(rawJson); //json을 list로 변환 APIVWAPState apiVWAPState = tickServiceManager.getService(ticker); long lastSeqId = lastSequentialIdMap.getOrDefault(ticker,0L); + boolean newTickAdded = false; //api 중복검사하여 queue에 저장하기 for (int i = gson.size()-1; i >=0 ; i--) {//2차 : 10 - 역순으로 정렬되어 - 순회해야 함. @@ -62,17 +62,29 @@ public void MarketDataRequest(String ticker){ if (ticks.getSequential_id() > lastSeqId){ //중복 검증용 apiVWAPState.addTick(ticks); lastSeqId = Math.max(lastSeqId, ticks.getSequential_id()); //중복 id 갱신 - + newTickAdded = true; } } lastSequentialIdMap.put(ticker,lastSeqId); + double vwap = apiVWAPState.getVWAP(); double volume = apiVWAPState.getAvgVolumePerOrder(); + double platformVwap = platformVWAPService.calculateVWAPbyTrades(ticker,vwap); + double difference = Math.abs((platformVwap - vwap)/vwap); recorder.recordApiVwap(ticker,vwap); - orderGenerateService.generateOrder(ticker,vwap,volume); //1tick 당 매수/매도 3개씩 제작 + if (difference>0.001){ + orderGenerateService.generateOrder(ticker,vwap,volume); //1tick 당 매수/매도 3개씩 제작 +// log.info("기준치 초과시 {}의 가격 : {} , 볼륨 : {}, 오차 : {}",ticker, vwap, volume, difference); + } else { + if (newTickAdded){ + orderGenerateService.generateOrder(ticker,vwap,volume); +// log.info("기준치 이내 {}의 가격 : {} , 볼륨 : {}, 오차 : {}",ticker, vwap, volume, difference); + } + } + + -// log.info("작동확인 {}의 가격 : {} , 볼륨 : {}",ticker, vwap, volume); } /* @Override diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/H2UnitPriceRefresher.java b/src/main/java/com/cleanengine/coin/realitybot/api/H2UnitPriceRefresher.java index 74c08104..d8750944 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/H2UnitPriceRefresher.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/H2UnitPriceRefresher.java @@ -10,11 +10,12 @@ @Slf4j @Component -@Profile("h2-mem") +@Profile({"(dev & !it & !mariadb-local) | h2-mem"}) @RequiredArgsConstructor public class H2UnitPriceRefresher implements ApplicationRunner { private final UnitPriceRefresher unitPriceRefresher; + @Override public void run(ApplicationArguments args){ log.info("Running Unit Price Refresher (h2-mem)..."); unitPriceRefresher.initializeUnitPrices(); diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java b/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java index aa801573..cfb7f6f6 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java @@ -31,7 +31,7 @@ public void addTick(Ticks tick){ //n초마다 5회 주문 , api 체결 내역에서 10종목씩 비교 public double getAvgVolumePerOrder() { - return calculator.getTotalVolume() / 50; + return calculator.getTotalVolume() / ticksQueue.size(); }//todo 에러 인젝션으로 50일때와 5일때 복귀 속도 알아보기 public double getVWAP(){ diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java index 4529fe97..e3e33b99 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -46,19 +46,20 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// recorder.recordPlatformVwap(ticker,platformVWAP); //편차 계산 (vwap 기준) double trendLineRate = (platformVWAP - apiVWAP)/ apiVWAP; + + for(int level : orderLevels) { //1주문당 3회 매수매도 처리 - OrderPricePolicy.OrderPrice basePrice = orderPricePolicy.calculatePrice(level,platformVWAP, unitPrice,trendLineRate); - DeviationPricePolicy.AdjustPrice adjustPrice = deviationPricePolicy.adjust( - basePrice.sell(), basePrice.buy(), trendLineRate, apiVWAP, unitPrice); + OrderPricePolicy.OrderPrice basePrice = orderPricePolicy.calculatePrice(level,apiVWAP,platformVWAP, unitPrice); +// DeviationPricePolicy.AdjustPrice adjustPrice = deviationPricePolicy.adjust(basePrice.sell(), basePrice.buy(), trendLineRate, apiVWAP, unitPrice); double sellVolume = orderVolumePolicy.calculateVolume(avgVolume,trendLineRate,false); double buyVolume = orderVolumePolicy.calculateVolume(avgVolume,trendLineRate,true); - double sellPrice = adjustPrice.sell(); - double buyPrice = adjustPrice.buy(); +// double sellPrice = adjustPrice.sell(); +// double buyPrice = adjustPrice.buy(); - createOrderWithFallback(ticker,false, sellVolume, sellPrice); - createOrderWithFallback(ticker,true, buyVolume, buyPrice); + createOrderWithFallback(ticker,false, sellVolume, basePrice.sell()); + createOrderWithFallback(ticker,true, buyVolume, basePrice.buy()); /* DecimalFormat df = new DecimalFormat("#,##0.00"); DecimalFormat dfv = new DecimalFormat("#,###.########"); diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java index 6ef0e471..4e20cb51 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java @@ -18,17 +18,16 @@ public class DeviationPricePolicy { public AdjustPrice adjust(double platformSell,double platformBuy, double trendLineRate, double apiVWAP, double unitPrice){ double deviation = Math.abs(trendLineRate);//음수값 보정 - if (deviation <= 0.017){ - return new AdjustPrice(platformSell,platformBuy); + // 1% 이내 편차는 조정 없이 원본 가격 반환 + if (deviation <= 0.001) { // 0.017 → 0.01로 변경 + return new AdjustPrice(platformSell, platformBuy); } + + // 1% 초과 시에만 조정 로직 실행 double weight = getCorrectionWeight(deviation); -// double closeness = 1-weight; // 보간 가중치: 0.7 ~ 1.0 -> 0.5 - double closeness; // 보간 가중치: 0.7 ~ 1.0 -> 0.5 - if (deviation > 0.07){ - closeness = Math.max(0.2, 1 - weight); - } else { - closeness = 0.01; - } + double closeness = (deviation > 0.07) + ? Math.max(0.2, 1 - weight) + : 0.01; // double targetVWAP = (trendLineRate > 0) //만약 closeness 를 0.5 입력시 중간값 // ? apiVWAP + (platformSell - apiVWAP) * closeness // 고평가 → platformSell(25000) → apiVWAP(16000) 사이 가중치 %로 유도 diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java index fefa8582..aa99b89c 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java @@ -7,55 +7,38 @@ public class OrderPricePolicy { /** * 레벨에 따라 매수/매도 가격을 계산합니다. * @param level 주문 강도 (1~5) - * @param platformVWAP 플랫폼 기준 평균 체결 가격 +// * @param platformVWAP 플랫폼 기준 평균 체결 가격 * @param unitPrice 호가 단위 - * @param trendLineRate 플랫폼과 API VWAP의 편차율 +// * @param trendLineRate 플랫폼과 API VWAP의 편차율 * @return PricePair (매도/매수 가격) */ - public OrderPrice calculatePrice(int level, - double platformVWAP, - double unitPrice, - double trendLineRate) { - double priceOffset = unitPrice * level; - double sellPrice, buyPrice; - double randomOffset = Math.abs(getRandomOffset(platformVWAP,getDynamicMaxRate(trendLineRate))); - double basePrice = normalizeToUnit(platformVWAP, unitPrice); //기준 가격 (호가 단위 정규화) + public OrderPrice calculatePrice(int level, double apiVWAP, double platformVWAP, double unitPrice) { + double basePrice = normalizeToUnit(apiVWAP, unitPrice); + double targetPrice = normalizeToUnit(platformVWAP, unitPrice); + // 🔥 점진적 접근: API VWAP에서 Platform VWAP로 20% 씩 이동 + double convergenceRate = 0.2; // 천천히 접근 + double adjustedBase = basePrice + (targetPrice - basePrice) * convergenceRate; - if (level == 1){ //1level일 경우 주문이 겹치도록 설정 - //체결을 위해 매수가 올리고, 매도가 내리는 계산 적용 - sellPrice = normalizeToUnit(basePrice - randomOffset,unitPrice); - buyPrice = normalizeToUnit(basePrice + randomOffset,unitPrice); - } - //2~3 단계 : orderbook 단위 주문 - else { - randomOffset = getRandomOffset(platformVWAP,0.001); - //체결 확률 증가용 코드 - sellPrice = normalizeToUnit(platformVWAP + priceOffset - randomOffset,unitPrice); - buyPrice = normalizeToUnit(platformVWAP - priceOffset + randomOffset,unitPrice); - //안정적인 스프레드 유지 -// sellPrice = normalizeToUnit(platformVWAP + priceOffset); -// buyPrice = normalizeToUnit(platformVWAP - priceOffset); + double priceOffset = unitPrice * level; + + if (level == 1) { + // 1레벨: 조정된 기준가 근처 체결 유도 + return new OrderPrice(adjustedBase + priceOffset/2, adjustedBase - priceOffset/2); + } else { + // 2~5레벨: 조정된 기준가 기준 스프레드 + return new OrderPrice(adjustedBase + priceOffset, adjustedBase - priceOffset); } - return new OrderPrice(sellPrice, buyPrice); } - private double getRandomOffset(double basePrice, double maxRate){ - //시장가에 해당하는 호가는 거래 체결 강하게 하기 위함 - double percent = (Math.random() * 2-1)*maxRate; + private double getRandomOffset(double basePrice, double maxRate) { + double percent = (Math.random() * 2 - 1) * maxRate; return basePrice * percent; } - private double getDynamicMaxRate(double trendLineRate) { - // 편차가 벌어지면 벌어질수록 보정폭 확대 - // 5% = 2.51의 가중치 - // 11% = 5.51의 가중치 - return 0.01 + Math.abs(trendLineRate) * 0.5; - } - - private int normalizeToUnit(double price, double unitPrice){ //호가단위로 변환 - return (int) ((double)(Math.round(price / unitPrice)) * unitPrice); + private double normalizeToUnit(double price, double unitPrice) { + return Math.round(price / unitPrice) * unitPrice; } - public record OrderPrice(double sell, double buy){} -} + public record OrderPrice(double sell, double buy) {} +} \ No newline at end of file diff --git a/src/main/java/com/cleanengine/coin/trade/entity/Trade.java b/src/main/java/com/cleanengine/coin/trade/entity/Trade.java index 9f6e11de..bac87d50 100644 --- a/src/main/java/com/cleanengine/coin/trade/entity/Trade.java +++ b/src/main/java/com/cleanengine/coin/trade/entity/Trade.java @@ -23,7 +23,6 @@ public class Trade { @Column(name = "trade_time", nullable = false) @CreationTimestamp - //생성 타임은 추후 논의 예정 private LocalDateTime tradeTime; @Column(name = "buy_user_id", nullable = false) diff --git a/src/main/resources/application-actuator.yml b/src/main/resources/application-actuator.yml index 167d9532..bd3c931b 100644 --- a/src/main/resources/application-actuator.yml +++ b/src/main/resources/application-actuator.yml @@ -9,4 +9,19 @@ management: prometheus: metrics: export: - enabled: true \ No newline at end of file + enabled: true +server: + tomcat: + mbeanregistry: + enabled: true + +spring: + jmx: + enabled: true + jpa: + properties: + hibernate: + generate_statistics: true +logging: + level: + org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN diff --git a/src/main/resources/application-loadtest.yml b/src/main/resources/application-loadtest.yml new file mode 100644 index 00000000..e5b0bc62 --- /dev/null +++ b/src/main/resources/application-loadtest.yml @@ -0,0 +1,5 @@ +spring: + sql: + init: + mode: always + data-locations: classpath:dataset/`if`.users.sql,classpath:dataset/`if`.account.sql,classpath:dataset/`if`.walletBTC.sql,classpath:dataset/`if`.walletTRUMP.sql diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 88ad9a86..a629bbcc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -52,6 +52,6 @@ server: forward-headers-strategy: native bot-handler: - fixed-rate: 5000 # 5초마다 실행 + fixed-rate: 1000 # 5초마다 실행 cron : "0 0 0 * * *" # 매일 자정마다 호가 - order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 + order-level : 1,2 #오더북 단계 설정 - 주문량 증가 diff --git a/src/test/java/com/cleanengine/coin/chart/handler/TradeEventHandlerTest.java b/src/test/java/com/cleanengine/coin/chart/handler/TradeEventHandlerTest.java new file mode 100644 index 00000000..056e72bb --- /dev/null +++ b/src/test/java/com/cleanengine/coin/chart/handler/TradeEventHandlerTest.java @@ -0,0 +1,321 @@ +package com.cleanengine.coin.chart.handler; + +import com.cleanengine.coin.chart.dto.PrevRateDto; +import com.cleanengine.coin.chart.dto.RealTimeDataDto; +import com.cleanengine.coin.chart.dto.TradeEventDto; +import com.cleanengine.coin.chart.service.ChartSubscriptionService; +import com.cleanengine.coin.chart.service.RealTimeDataPrevRateService; +import com.cleanengine.coin.chart.service.RealTimeTradeService; +import com.cleanengine.coin.chart.service.WebsocketSendService; +import com.cleanengine.coin.trade.application.TradeExecutedEvent; +import com.cleanengine.coin.trade.entity.Trade; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("TradeEventHandler 단위 테스트") +class TradeEventHandlerTest { + + @Mock + private RealTimeTradeService realTimeTradeService; + + @Mock + private WebsocketSendService websocketSendService; + + @Mock + private ChartSubscriptionService chartSubscriptionService; + + @Mock + private RealTimeDataPrevRateService realTimeDataPrevRateService; + + @InjectMocks + private TradeEventHandler tradeEventHandler; + + private Trade validTrade; + private TradeExecutedEvent validEvent; + private LocalDateTime testTime; + + @BeforeEach + void setUp() { + testTime = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + validTrade = createTrade("BTC", 50000.0, 1.5, testTime); validEvent = TradeExecutedEvent.of(validTrade, 1L, 2L); + validEvent = TradeExecutedEvent.of(validTrade, 1L, 2L); + } + + // ===== handleTradeEvent 테스트 ===== + + @Test + @DisplayName("정상적인 거래 이벤트 처리 - 모든 구독자 있음") + void handleTradeEvent_ValidEvent_AllSubscribersPresent() { + // given + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + + RealTimeDataDto realTimeDto = createRealTimeDataDto(); + PrevRateDto prevRateDto = createPrevRateDto(); + + when(realTimeTradeService.generateRealTimeData(any(TradeEventDto.class))).thenReturn(realTimeDto); + when(realTimeDataPrevRateService.generatePrevRateData(any(TradeEventDto.class))).thenReturn(prevRateDto); + + // when + tradeEventHandler.handleTradeEvent(validEvent); + + // then + verify(chartSubscriptionService).isSubscribedToRealTimeTradeRate("BTC"); + verify(chartSubscriptionService).isSubscribedToPrevRate("BTC"); + verify(realTimeTradeService).generateRealTimeData(any(TradeEventDto.class)); + verify(realTimeDataPrevRateService).generatePrevRateData(any(TradeEventDto.class)); + verify(websocketSendService).sendChangeRate(realTimeDto, "BTC"); + verify(websocketSendService).sendPrevRate(prevRateDto, "BTC"); + } + + @Test + @DisplayName("정상적인 거래 이벤트 처리 - 구독자 없음") + void handleTradeEvent_ValidEvent_NoSubscribers() { + // given + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(false); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(false); + + // when + tradeEventHandler.handleTradeEvent(validEvent); + + // then + verify(chartSubscriptionService).isSubscribedToRealTimeTradeRate("BTC"); + verify(chartSubscriptionService).isSubscribedToPrevRate("BTC"); + verifyNoInteractions(realTimeTradeService); + verifyNoInteractions(realTimeDataPrevRateService); + verifyNoInteractions(websocketSendService); + } + + @Test + @DisplayName("null Trade 이벤트 처리") + void handleTradeEvent_NullTrade_HandlesGracefully() { + // given + TradeExecutedEvent nullTradeEvent = TradeExecutedEvent.of(null, null, null); + // when + tradeEventHandler.handleTradeEvent(nullTradeEvent); + + // then + verifyNoInteractions(chartSubscriptionService); + verifyNoInteractions(realTimeTradeService); + verifyNoInteractions(realTimeDataPrevRateService); + verifyNoInteractions(websocketSendService); + } + + @Test + @DisplayName("실시간 트레이드 처리 중 예외 발생") + void handleTradeEvent_RealTimeTradeException_ContinuesProcessing() { + // given + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + when(realTimeTradeService.generateRealTimeData(any(TradeEventDto.class))) + .thenThrow(new RuntimeException("실시간 데이터 생성 실패")); + + PrevRateDto prevRateDto = createPrevRateDto(); + when(realTimeDataPrevRateService.generatePrevRateData(any(TradeEventDto.class))).thenReturn(prevRateDto); + + // when + tradeEventHandler.handleTradeEvent(validEvent); + + // then + verify(realTimeTradeService).generateRealTimeData(any(TradeEventDto.class)); + verify(realTimeDataPrevRateService).generatePrevRateData(any(TradeEventDto.class)); + verify(websocketSendService, never()).sendChangeRate(any(), any()); + verify(websocketSendService).sendPrevRate(prevRateDto, "BTC"); + } + + @Test + @DisplayName("전일 대비 변동률 처리 중 예외 발생") + void handleTradeEvent_PrevRateException_ContinuesProcessing() { + // given + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + + RealTimeDataDto realTimeDto = createRealTimeDataDto(); + when(realTimeTradeService.generateRealTimeData(any(TradeEventDto.class))).thenReturn(realTimeDto); + when(realTimeDataPrevRateService.generatePrevRateData(any(TradeEventDto.class))) + .thenThrow(new RuntimeException("전일 대비 변동률 생성 실패")); + + // when + tradeEventHandler.handleTradeEvent(validEvent); + + // then + verify(realTimeTradeService).generateRealTimeData(any(TradeEventDto.class)); + verify(realTimeDataPrevRateService).generatePrevRateData(any(TradeEventDto.class)); + verify(websocketSendService).sendChangeRate(realTimeDto, "BTC"); + verify(websocketSendService, never()).sendPrevRate(any(), any()); + } + + // ===== processRealTimeTradeRate 테스트 ===== + + @Test + @DisplayName("실시간 트레이드 처리 - 구독자 있음") + void processRealTimeTradeRate_WithSubscribers_SendsData() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + RealTimeDataDto realTimeDto = createRealTimeDataDto(); + + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(realTimeTradeService.generateRealTimeData(tradeEventDto)).thenReturn(realTimeDto); + + // when + tradeEventHandler.processRealTimeTradeRate("BTC", tradeEventDto); + + // then + verify(chartSubscriptionService).isSubscribedToRealTimeTradeRate("BTC"); + verify(realTimeTradeService).generateRealTimeData(tradeEventDto); + verify(websocketSendService).sendChangeRate(realTimeDto, "BTC"); + } + + @Test + @DisplayName("실시간 트레이드 처리 - 구독자 없음") + void processRealTimeTradeRate_NoSubscribers_SkipsProcessing() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(false); + + // when + tradeEventHandler.processRealTimeTradeRate("BTC", tradeEventDto); + + // then + verify(chartSubscriptionService).isSubscribedToRealTimeTradeRate("BTC"); + verifyNoInteractions(realTimeTradeService); + verifyNoInteractions(websocketSendService); + } + + @Test + @DisplayName("실시간 트레이드 처리 중 예외 발생 시 로그 출력") + void processRealTimeTradeRate_Exception_LogsError() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + when(chartSubscriptionService.isSubscribedToRealTimeTradeRate("BTC")).thenReturn(true); + when(realTimeTradeService.generateRealTimeData(tradeEventDto)) + .thenThrow(new RuntimeException("데이터 생성 실패")); + + // when + tradeEventHandler.processRealTimeTradeRate("BTC", tradeEventDto); + + // then + verify(realTimeTradeService).generateRealTimeData(tradeEventDto); + verify(websocketSendService, never()).sendChangeRate(any(), any()); + } + + // ===== processPrevRateData 테스트 ===== + + @Test + @DisplayName("전일 대비 변동률 처리 - 구독자 있음") + void processPrevRateData_WithSubscribers_SendsData() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + PrevRateDto prevRateDto = createPrevRateDto(); + + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + when(realTimeDataPrevRateService.generatePrevRateData(tradeEventDto)).thenReturn(prevRateDto); + + // when + tradeEventHandler.processPrevRateData("BTC", tradeEventDto); + + // then + verify(chartSubscriptionService).isSubscribedToPrevRate("BTC"); + verify(realTimeDataPrevRateService).generatePrevRateData(tradeEventDto); + verify(websocketSendService).sendPrevRate(prevRateDto, "BTC"); + } + + @Test + @DisplayName("전일 대비 변동률 처리 - 구독자 없음") + void processPrevRateData_NoSubscribers_SkipsProcessing() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(false); + + // when + tradeEventHandler.processPrevRateData("BTC", tradeEventDto); + + // then + verify(chartSubscriptionService).isSubscribedToPrevRate("BTC"); + verifyNoInteractions(realTimeDataPrevRateService); + verifyNoInteractions(websocketSendService); + } + + @Test + @DisplayName("전일 대비 변동률 처리 중 예외 발생 시 로그 출력") + void processPrevRateData_Exception_LogsError() { + // given + TradeEventDto tradeEventDto = createTradeEventDto(); + when(chartSubscriptionService.isSubscribedToPrevRate("BTC")).thenReturn(true); + when(realTimeDataPrevRateService.generatePrevRateData(tradeEventDto)) + .thenThrow(new RuntimeException("변동률 계산 실패")); + + // when + tradeEventHandler.processPrevRateData("BTC", tradeEventDto); + + // then + verify(realTimeDataPrevRateService).generatePrevRateData(tradeEventDto); + verify(websocketSendService, never()).sendPrevRate(any(), any()); + } + + // ===== getTradeEventDto 정적 메서드 테스트 ===== + + @Test + @DisplayName("Trade에서 TradeEventDto 생성") + void getTradeEventDto_ValidTrade_CreatesDto() { + // when + TradeEventDto result = TradeEventHandler.getTradeEventDto(validTrade); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTicker()).isEqualTo("BTC"); + assertThat(result.getSize()).isEqualTo(1.5); + assertThat(result.getPrice()).isEqualTo(50000.0); + assertThat(result.getTimestamp()).isEqualTo(testTime); + } + // ===== 헬퍼 메서드 ===== + + private Trade createTrade(String ticker, Double price, Double size, LocalDateTime tradeTime) { + return new Trade( + 1, // id + ticker, // ticker + tradeTime, // tradeTime + 1, // buyUserId + 2, // sellUserId + price, // price + size // size + ); + } + + private TradeEventDto createTradeEventDto() { + return new TradeEventDto("BTC", 1.5, 50000.0, testTime); + } + + private RealTimeDataDto createRealTimeDataDto() { + return new RealTimeDataDto( + "BTC", + 1.5, + 50000.0, + 2.5, + testTime, + "test-transaction-id" + ); + } + + private PrevRateDto createPrevRateDto() { + return new PrevRateDto( + "BTC", + 50000.0, + 48000.0, + 4.17, + testTime + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/chart/repository/MinuteOhlcDataRepositoryTest.java b/src/test/java/com/cleanengine/coin/chart/repository/MinuteOhlcDataRepositoryTest.java new file mode 100644 index 00000000..635a45c3 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/chart/repository/MinuteOhlcDataRepositoryTest.java @@ -0,0 +1,67 @@ +package com.cleanengine.coin.chart.repository; + +import com.cleanengine.coin.trade.entity.Trade; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + + +@DataJpaTest +class MinuteOhlcDataRepositoryTest { + + @Autowired + private MinuteOhlcDataRepository repository; + + @DisplayName("db를 ticker로 모든 트레이드를 시간 순으로 조회") + @Test + public void findByTickerOrderByTradeTimeAsc() throws Exception { + //given + Trade trade1 = new Trade(); + trade1.setTicker("BTC"); + trade1.setSize(1.0); + trade1.setPrice(10000.0); + trade1.setTradeTime(LocalDateTime.now()); + trade1.setBuyUserId(Integer.valueOf("1")); + trade1.setSellUserId(Integer.valueOf("2")); + + Trade trade2 = new Trade(); + trade2.setTicker("BTC"); + trade2.setSize(2.0); + trade2.setPrice(20000.0); + trade2.setTradeTime(LocalDateTime.now()); + trade2.setBuyUserId(3); + trade2.setSellUserId(4); + + + Trade trade3 = new Trade(); + trade3.setTicker("BTC"); + trade3.setSize(3.0); + trade3.setPrice(30000.0); + trade3.setTradeTime(LocalDateTime.now()); + trade3.setBuyUserId(5); + trade3.setSellUserId(6); + + repository.saveAll(List.of(trade1, trade2, trade3)); + + + //when + List result = repository.findByTickerOrderByTradeTimeAsc("BTC"); + + //then + assertThat(result).hasSize(3) + .extracting("ticker", "size", "price", "TradeTime") + .containsExactlyInAnyOrder( + tuple("BTC", 1.0, 10000.0, trade1.getTradeTime()), + tuple("BTC", 2.0, 20000.0, trade2.getTradeTime()), + tuple("BTC", 3.0, 30000.0, trade3.getTradeTime()) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/chart/repository/RealTimeTradeRepositoryTest.java b/src/test/java/com/cleanengine/coin/chart/repository/RealTimeTradeRepositoryTest.java new file mode 100644 index 00000000..3015ebcc --- /dev/null +++ b/src/test/java/com/cleanengine/coin/chart/repository/RealTimeTradeRepositoryTest.java @@ -0,0 +1,77 @@ +package com.cleanengine.coin.chart.repository; + +import com.cleanengine.coin.trade.entity.Trade; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +//jpa의 시간 테스트를 할때 + + +@DataJpaTest +class RealTimeTradeRepositoryTest { + + private LocalDateTime today; + private LocalDateTime yesterdayStart; + private LocalDateTime yesterdayEnd; + + @BeforeEach + void setUp() { + + today = LocalDateTime.now(); + yesterdayStart = today.minusDays(1).withHour(0).withMinute(0).withSecond(0); + yesterdayEnd = today.minusDays(1).withHour(23).withMinute(59).withSecond(59); + } + + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private RealTimeTradeRepository repository; + + @DisplayName("특정 기간 내 가장 마지막 거래를 올바르게 조회") + @Test + public void findFirstByTickerAndTradeTimeBetweenOrderByTradeTimeDesc() throws Exception { + // given - 네이티브 쿼리로 정확한 시간 설정 + LocalDateTime time1 = yesterdayStart.plusHours(10); + LocalDateTime time2 = yesterdayStart.plusHours(15); + LocalDateTime time3 = yesterdayStart.plusHours(20); + + // 직접 SQL로 데이터 삽입 + entityManager.getEntityManager() + .createNativeQuery("INSERT INTO trade (ticker, trade_time, buy_user_id, sell_user_id, price, size) VALUES (?, ?, ?, ?, ?, ?)") + .setParameter(1, "BTC").setParameter(2, time1).setParameter(3, 1).setParameter(4, 2).setParameter(5, 50000.0).setParameter(6, 1.0) + .executeUpdate(); + + entityManager.getEntityManager() + .createNativeQuery("INSERT INTO trade (ticker, trade_time, buy_user_id, sell_user_id, price, size) VALUES (?, ?, ?, ?, ?, ?)") + .setParameter(1, "BTC").setParameter(2, time2).setParameter(3, 1).setParameter(4, 2).setParameter(5, 52000.0).setParameter(6, 2.0) + .executeUpdate(); + + entityManager.getEntityManager() + .createNativeQuery("INSERT INTO trade (ticker, trade_time, buy_user_id, sell_user_id, price, size) VALUES (?, ?, ?, ?, ?, ?)") + .setParameter(1, "BTC").setParameter(2, time3).setParameter(3, 1).setParameter(4, 2).setParameter(5, 51500.0).setParameter(6, 3.0) + .executeUpdate(); + + entityManager.flush(); + + // when + Trade result = repository.findFirstByTickerAndTradeTimeBetweenOrderByTradeTimeDesc( + "BTC", yesterdayStart, yesterdayEnd); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTicker()).isEqualTo("BTC"); + assertThat(result.getPrice()).isEqualTo(51500.0); // 가장 마지막 시간의 거래 + } + + + +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/chart/service/ChartSubscriptionServiceTest.java b/src/test/java/com/cleanengine/coin/chart/service/ChartSubscriptionServiceTest.java index a3ac5db7..be1eb7ed 100644 --- a/src/test/java/com/cleanengine/coin/chart/service/ChartSubscriptionServiceTest.java +++ b/src/test/java/com/cleanengine/coin/chart/service/ChartSubscriptionServiceTest.java @@ -349,20 +349,20 @@ void unsubscribeWithNullTicker_ThrowsException() { } @Test - @DisplayName("유효하지 않은 티커의 구독 상태 확인 시 false를 반환한다") - void isSubscribedWithInvalidTicker_ReturnsFalse() { - // when & then - assertThat(service.isSubscribedToRealTimeTradeRate(null)).isFalse(); - assertThat(service.isSubscribedToRealTimeTradeRate("")).isFalse(); - assertThat(service.isSubscribedToRealTimeTradeRate(" ")).isFalse(); + @DisplayName("유효하지 않은 티커의 구독 상태 확인 시 예외가 발생한다") + void isSubscribedWithInvalidTicker_ThrowsException() { + assertThatThrownBy(() -> service.isSubscribedToRealTimeTradeRate(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유효하지 않은 티커입니다"); + + assertThatThrownBy(() -> service.isSubscribedToRealTimeTradeRate("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유효하지 않은 티커입니다"); - assertThat(service.isSubscribedToRealTimeOhlc(null)).isFalse(); - assertThat(service.isSubscribedToRealTimeOhlc("")).isFalse(); - assertThat(service.isSubscribedToRealTimeOhlc(" ")).isFalse(); + assertThatThrownBy(() -> service.isSubscribedToRealTimeTradeRate(" ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유효하지 않은 티커입니다"); - assertThat(service.isSubscribedToPrevRate(null)).isFalse(); - assertThat(service.isSubscribedToPrevRate("")).isFalse(); - assertThat(service.isSubscribedToPrevRate(" ")).isFalse(); } // ===== 엣지 케이스 테스트 ===== diff --git a/src/test/java/com/cleanengine/coin/chart/service/RealTimeOhlcServiceTest.java b/src/test/java/com/cleanengine/coin/chart/service/RealTimeOhlcServiceTest.java index b516bed8..a9fb8c9e 100644 --- a/src/test/java/com/cleanengine/coin/chart/service/RealTimeOhlcServiceTest.java +++ b/src/test/java/com/cleanengine/coin/chart/service/RealTimeOhlcServiceTest.java @@ -3,420 +3,132 @@ import com.cleanengine.coin.chart.dto.RealTimeOhlcDto; import com.cleanengine.coin.trade.entity.Trade; import com.cleanengine.coin.trade.repository.TradeRepository; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -@DisplayName("RealTimeOhlcService 단위 테스트") class RealTimeOhlcServiceTest { @Mock private TradeRepository tradeRepository; @InjectMocks - private RealTimeOhlcService service; + private RealTimeOhlcService realTimeOhlcService; - private String validTicker; - private LocalDateTime fixedNow; - private List mockTrades; - - @BeforeEach - void setUp() { - validTicker = "BTC"; - fixedNow = LocalDateTime.of(2024, 1, 15, 10, 30, 0); - mockTrades = createMockTrades(); - } - - // ===== getRealTimeOhlc 통합 테스트 ===== - @Test - @DisplayName("정상적인 거래 데이터로 실시간 OHLC를 생성한다") - void getRealTimeOhlc_WithValidTrades_ReturnsOhlcData() { - // given - when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc( - eq(validTicker), any(LocalDateTime.class), any(LocalDateTime.class))) - .thenReturn(mockTrades); - - // when - RealTimeOhlcDto result = service.getRealTimeOhlc(validTicker); - - // then - assertThat(result).isNotNull(); - assertThat(result.getTicker()).isEqualTo("BTC"); - assertThat(result.getHigh()).isEqualTo(200.0); - assertThat(result.getLow()).isEqualTo(100.0); - assertThat(result.getVolume()).isEqualTo(6.0); // 1+2+3 - } + private final String ticker = "KRW-BTC"; + private final Integer dummyBuyUserId = 100; + private final Integer dummySellUserId = 200; @Test - @DisplayName("거래 데이터가 없으면 캐시된 데이터를 반환한다") - void getRealTimeOhlc_NoTrades_ReturnsCachedData() { + @DisplayName("새로운 1분봉 - 첫 거래 발생 시 OHLCV가 정상적으로 생성되어야 한다") + void should_create_new_ohlc_when_new_minute_starts() { // given - when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc( - eq(validTicker), any(LocalDateTime.class), any(LocalDateTime.class))) - .thenReturn(List.of()); - - // 캐시에 데이터 미리 저장 - RealTimeOhlcDto cachedData = new RealTimeOhlcDto(validTicker, fixedNow, 100.0, 100.0, 100.0, 100.0, 5.0); - service.updateCache(validTicker, fixedNow, cachedData); - - // when - RealTimeOhlcDto result = service.getRealTimeOhlc(validTicker); + LocalDateTime now = LocalDateTime.of(2025, 6, 22, 10, 1, 15); + LocalDateTime minuteStart = now.truncatedTo(ChronoUnit.MINUTES); - // then - assertThat(result).isEqualTo(cachedData); - } - - @Test - @DisplayName("거래 데이터도 캐시도 없으면 null을 반환한다") - void getRealTimeOhlc_NoTradesNoCache_ReturnsNull() { - // given - when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc( - eq(validTicker), any(LocalDateTime.class), any(LocalDateTime.class))) - .thenReturn(List.of()); - - // when - RealTimeOhlcDto result = service.getRealTimeOhlc(validTicker); - - // then - assertThat(result).isNull(); - } - - @Test - @DisplayName("예외 발생 시 캐시된 데이터를 반환한다") - void getRealTimeOhlc_ExceptionOccurs_ReturnsCachedData() { - // given - when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc( - eq(validTicker), any(LocalDateTime.class), any(LocalDateTime.class))) - .thenThrow(new RuntimeException("DB 연결 오류")); - - RealTimeOhlcDto cachedData = new RealTimeOhlcDto(validTicker, fixedNow, 100.0, 100.0, 100.0, 100.0, 5.0); - service.updateCache(validTicker, fixedNow, cachedData); - - // when - RealTimeOhlcDto result = service.getRealTimeOhlc(validTicker); - - // then - assertThat(result).isEqualTo(cachedData); - } - - // ===== calculateTimeRange 테스트 ===== - @Test - @DisplayName("첫 번째 호출 시 1초 전부터 현재까지의 범위를 계산한다") - void calculateTimeRange_FirstCall_ReturnsOneSecondRange() { - // when - RealTimeOhlcService.TimeRange result = service.calculateTimeRange(validTicker, fixedNow); - - // then - assertThat(result.start()).isEqualTo(fixedNow.minusSeconds(1)); - assertThat(result.end()).isEqualTo(fixedNow); - } - - @Test - @DisplayName("이전 처리 시간이 있으면 그 시간부터 현재까지의 범위를 계산한다") - void calculateTimeRange_WithPreviousTime_ReturnsCustomRange() { - // given - LocalDateTime previousTime = fixedNow.minusSeconds(5); - - // null 대신 더미 데이터 사용 - RealTimeOhlcDto dummyData = new RealTimeOhlcDto( - validTicker, previousTime, 100.0, 100.0, 100.0, 100.0, 1.0 + List trades = List.of( + new Trade(ticker, now.minusSeconds(10), dummyBuyUserId, dummySellUserId, 50000.0, 10.0) // 10:01:05 ); - service.updateCache(validTicker, previousTime, dummyData); // ✅ 유효한 객체 - - // when - RealTimeOhlcService.TimeRange result = service.calculateTimeRange(validTicker, fixedNow); - - // then - assertThat(result.start()).isEqualTo(previousTime); - assertThat(result.end()).isEqualTo(fixedNow); - } - - // ===== getProcessedTradeData 테스트 ===== - @Test - @DisplayName("빈 거래 데이터는 빈 리스트를 반환한다") - void getProcessedTradeData_EmptyTrades_ReturnsEmptyList() { - // given - RealTimeOhlcService.TimeRange timeRange = new RealTimeOhlcService.TimeRange( - fixedNow.minusSeconds(1), fixedNow); - - when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc( - validTicker, timeRange.start(), timeRange.end())) - .thenReturn(List.of()); - // when - List result = service.getProcessedTradeData(validTicker, timeRange); - - // then - assertThat(result).isEmpty(); - } - - // ===== updateCache 테스트 ===== - @Test - @DisplayName("캐시를 정상적으로 업데이트한다") - void updateCache_ValidData_UpdatesCorrectly() { - // given - RealTimeOhlcDto ohlcData = new RealTimeOhlcDto(validTicker, fixedNow, 100.0, 200.0, 50.0, 150.0, 10.0); + when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(eq(ticker), eq(minuteStart), eq(now))) + .thenReturn(trades); // when - service.updateCache(validTicker, fixedNow, ohlcData); + RealTimeOhlcDto result = realTimeOhlcService.getAndUpdateCumulative1mOhlc(ticker, now); // then - RealTimeOhlcDto cachedData = service.getCachedData(validTicker); - assertThat(cachedData).isEqualTo(ohlcData); - - // 시간 범위 계산 시 업데이트된 시간이 사용되는지 확인 - RealTimeOhlcService.TimeRange timeRange = service.calculateTimeRange(validTicker, fixedNow.plusSeconds(5)); - assertThat(timeRange.start()).isEqualTo(fixedNow); - } - - // ===== getCachedData 테스트 ===== - @Test - @DisplayName("캐시된 데이터를 정상적으로 조회한다") - void getCachedData_ExistingData_ReturnsData() { - // given - RealTimeOhlcDto expectedData = new RealTimeOhlcDto(validTicker, fixedNow, 100.0, 200.0, 50.0, 150.0, 10.0); - service.updateCache(validTicker, fixedNow, expectedData); - - // when - RealTimeOhlcDto result = service.getCachedData(validTicker); - - // then - assertThat(result).isEqualTo(expectedData); - } - - @Test - @DisplayName("캐시에 데이터가 없으면 null을 반환한다") - void getCachedData_NoData_ReturnsNull() { - // when - RealTimeOhlcDto result = service.getCachedData("NONEXISTENT"); - - // then - assertThat(result).isNull(); - } - - // ===== createOhlcDto 테스트 ===== - @Test - @DisplayName("OHLCV 데이터로 DTO를 생성한다") - void createOhlcDto_ValidData_CreatesCorrectDto() { - // given - RealTimeOhlcService.calculateOhlcv ohlcv = - new RealTimeOhlcService.calculateOhlcv(100.0, 200.0, 50.0, 150.0, 10.0); - - // when - RealTimeOhlcDto result = service.createOhlcDto(validTicker, fixedNow, ohlcv); - - // then - assertThat(result.getTicker()).isEqualTo(validTicker); - assertThat(result.getTimestamp()).isEqualTo(fixedNow); - assertThat(result.getOpen()).isEqualTo(100.0); - assertThat(result.getHigh()).isEqualTo(200.0); - assertThat(result.getLow()).isEqualTo(50.0); - assertThat(result.getClose()).isEqualTo(150.0); + assertThat(result).isNotNull(); + assertThat(result.getOpen()).isEqualTo(50000.0); + assertThat(result.getHigh()).isEqualTo(50000.0); + assertThat(result.getLow()).isEqualTo(50000.0); + assertThat(result.getClose()).isEqualTo(50000.0); assertThat(result.getVolume()).isEqualTo(10.0); } - // ===== getCalculateOhlcv 정적 메서드 테스트 ===== - @Test - @DisplayName("단일 거래로 OHLCV를 계산한다") - void getCalculateOhlcv_SingleTrade_CalculatesCorrectly() { - // given - List trades = List.of( - createTrade(fixedNow, 100.0, 5.0) - ); - - // when - RealTimeOhlcService.calculateOhlcv result = - RealTimeOhlcService.getCalculateOhlcv(trades); - - // then - assertThat(result.open()).isEqualTo(100.0); - assertThat(result.high()).isEqualTo(100.0); - assertThat(result.low()).isEqualTo(100.0); - assertThat(result.close()).isEqualTo(100.0); - assertThat(result.volume()).isEqualTo(5.0); - } - @Test - @DisplayName("여러 거래로 OHLCV를 계산한다") - void getCalculateOhlcv_MultipleTrades_CalculatesCorrectly() { + @DisplayName("기존 1분봉 - 새로운 거래 발생 시 OHLCV가 누적 업데이트되어야 한다") + void should_update_existing_ohlc_on_new_trades_in_same_minute() { // given - List trades = List.of( - createTrade(fixedNow.minusSeconds(3), 100.0, 1.0), // open - createTrade(fixedNow.minusSeconds(2), 200.0, 2.0), // high - createTrade(fixedNow.minusSeconds(1), 50.0, 3.0), // low - createTrade(fixedNow, 150.0, 4.0) // close - ); - - // when - RealTimeOhlcService.calculateOhlcv result = - RealTimeOhlcService.getCalculateOhlcv(trades); + // 1. 첫 번째 거래 발생 (10:01:15) + LocalDateTime time1 = LocalDateTime.of(2025, 6, 22, 10, 1, 15); + LocalDateTime minuteStart = time1.truncatedTo(ChronoUnit.MINUTES); - // then - assertThat(result.open()).isEqualTo(100.0); - assertThat(result.high()).isEqualTo(200.0); - assertThat(result.low()).isEqualTo(50.0); - assertThat(result.close()).isEqualTo(150.0); - assertThat(result.volume()).isEqualTo(10.0); // 1+2+3+4 - } - - @Test - @DisplayName("동일한 가격의 거래들로 OHLCV를 계산한다") - void getCalculateOhlcv_SamePriceTrades_CalculatesCorrectly() { - // given - List trades = List.of( - createTrade(fixedNow.minusSeconds(2), 100.0, 1.0), - createTrade(fixedNow.minusSeconds(1), 100.0, 2.0), - createTrade(fixedNow, 100.0, 3.0) + List initialTrades = List.of( + new Trade(ticker, time1.minusSeconds(10), dummyBuyUserId, dummySellUserId, 50000.0, 10.0) // 10:01:05 ); + when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(eq(ticker), eq(minuteStart), eq(time1))) + .thenReturn(initialTrades); - // when - RealTimeOhlcService.calculateOhlcv result = - RealTimeOhlcService.getCalculateOhlcv(trades); + realTimeOhlcService.getAndUpdateCumulative1mOhlc(ticker, time1); // 첫 호출로 상태 초기화 - // then - assertThat(result.open()).isEqualTo(100.0); - assertThat(result.high()).isEqualTo(100.0); - assertThat(result.low()).isEqualTo(100.0); - assertThat(result.close()).isEqualTo(100.0); - assertThat(result.volume()).isEqualTo(6.0); - } + // 2. 두 번째 거래들 발생 (10:01:30) + LocalDateTime time2 = LocalDateTime.of(2025, 6, 22, 10, 1, 30); - @Test - @DisplayName("소수점 가격과 거래량으로 정확하게 계산한다") - void getCalculateOhlcv_DecimalValues_CalculatesCorrectly() { - // given - List trades = List.of( - createTrade(fixedNow.minusSeconds(1), 100.5, 1.5), - createTrade(fixedNow, 200.75, 2.25) + List newTrades = List.of( + new Trade(ticker, time2.minusSeconds(10), dummyBuyUserId, dummySellUserId, 52000.0, 5.0), // 10:01:20 (고가) + new Trade(ticker, time2.minusSeconds(5), dummyBuyUserId, dummySellUserId, 49000.0, 8.0) // 10:01:25 (저가, 종가) ); + when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(eq(ticker), eq(time1), eq(time2))) + .thenReturn(newTrades); // when - RealTimeOhlcService.calculateOhlcv result = - RealTimeOhlcService.getCalculateOhlcv(trades); + RealTimeOhlcDto result = realTimeOhlcService.getAndUpdateCumulative1mOhlc(ticker, time2); // then - assertThat(result.open()).isEqualTo(100.5); - assertThat(result.high()).isEqualTo(200.75); - assertThat(result.low()).isEqualTo(100.5); - assertThat(result.close()).isEqualTo(200.75); - assertThat(result.volume()).isEqualTo(3.75); // 1.5 + 2.25 - } - - // ===== 레코드 객체 테스트 ===== - @Test - @DisplayName("TimeRange 레코드가 올바르게 동작한다") - void timeRangeRecord_WorksCorrectly() { - // given - LocalDateTime start = fixedNow.minusSeconds(1); - LocalDateTime end = fixedNow; - - // when - RealTimeOhlcService.TimeRange timeRange = new RealTimeOhlcService.TimeRange(start, end); - - // then - assertThat(timeRange.start()).isEqualTo(start); - assertThat(timeRange.end()).isEqualTo(end); - assertThat(timeRange.toString()).contains(start.toString(), end.toString()); - } - - @Test - @DisplayName("calculateOhlcv 레코드가 올바르게 동작한다") - void calculateOhlcvRecord_WorksCorrectly() { - // given - RealTimeOhlcService.calculateOhlcv ohlcv = - new RealTimeOhlcService.calculateOhlcv(100.0, 200.0, 50.0, 150.0, 10.0); - - // then - assertThat(ohlcv.open()).isEqualTo(100.0); - assertThat(ohlcv.high()).isEqualTo(200.0); - assertThat(ohlcv.low()).isEqualTo(50.0); - assertThat(ohlcv.close()).isEqualTo(150.0); - assertThat(ohlcv.volume()).isEqualTo(10.0); - assertThat(ohlcv.toString()).contains("100.0", "200.0", "50.0", "150.0", "10.0"); + assertThat(result).isNotNull(); + assertThat(result.getOpen()).isEqualTo(50000.0); // 시가 불변 + assertThat(result.getHigh()).isEqualTo(52000.0); // 고가 갱신 + assertThat(result.getLow()).isEqualTo(49000.0); // 저가 갱신 + assertThat(result.getClose()).isEqualTo(49000.0); // 종가 갱신 + assertThat(result.getVolume()).isEqualTo(23.0); // 거래량 누적 (10 + 5 + 8) } - // ===== 동시성 테스트 ===== @Test - @DisplayName("여러 티커를 동시에 처리해도 캐시가 올바르게 동작한다") - void concurrentTickers_CacheWorksCorrectly() { + @DisplayName("시간 경과 - 다음 '분'으로 넘어갈 시 새로운 1분봉이 시작되어야 한다") + void should_start_new_ohlc_when_minute_rolls_over() { // given - String ticker1 = "BTC"; - String ticker2 = "ETH"; - RealTimeOhlcDto data1 = new RealTimeOhlcDto(ticker1, fixedNow, 100.0, 100.0, 100.0, 100.0, 5.0); - RealTimeOhlcDto data2 = new RealTimeOhlcDto(ticker2, fixedNow, 200.0, 200.0, 200.0, 200.0, 10.0); + // 1. 10:01분대의 마지막 상태 설정 + LocalDateTime time1 = LocalDateTime.of(2025, 6, 22, 10, 1, 50); + LocalDateTime minute1Start = time1.truncatedTo(ChronoUnit.MINUTES); - // when - service.updateCache(ticker1, fixedNow, data1); - service.updateCache(ticker2, fixedNow, data2); + List tradesInMin1 = List.of( + new Trade(ticker, time1.minusSeconds(10), dummyBuyUserId, dummySellUserId, 50000.0, 10.0) + ); + when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(eq(ticker), eq(minute1Start), eq(time1))) + .thenReturn(tradesInMin1); + realTimeOhlcService.getAndUpdateCumulative1mOhlc(ticker, time1); - // then - assertThat(service.getCachedData(ticker1)).isEqualTo(data1); - assertThat(service.getCachedData(ticker2)).isEqualTo(data2); - assertThat(service.getCachedData(ticker1)).isNotEqualTo(data2); - } + // 2. 10:02분대의 첫 거래 발생 + LocalDateTime time2 = LocalDateTime.of(2025, 6, 22, 10, 2, 10); + LocalDateTime minute2Start = time2.truncatedTo(ChronoUnit.MINUTES); - // ===== Repository 호출 검증 테스트 ===== - @Test - @DisplayName("Repository가 올바른 파라미터로 호출된다") - void repository_CalledWithCorrectParameters() { - // given - when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc( - any(String.class), any(LocalDateTime.class), any(LocalDateTime.class))) - .thenReturn(mockTrades); + List tradesInMin2 = List.of( + new Trade(ticker, time2.minusSeconds(5), dummyBuyUserId, dummySellUserId, 51500.0, 20.0) + ); + when(tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(eq(ticker), eq(minute2Start), eq(time2))) + .thenReturn(tradesInMin2); // when - service.getRealTimeOhlc(validTicker); + RealTimeOhlcDto result = realTimeOhlcService.getAndUpdateCumulative1mOhlc(ticker, time2); // then - ArgumentCaptor tickerCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor startCaptor = ArgumentCaptor.forClass(LocalDateTime.class); - ArgumentCaptor endCaptor = ArgumentCaptor.forClass(LocalDateTime.class); - - verify(tradeRepository).findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc( - tickerCaptor.capture(), startCaptor.capture(), endCaptor.capture()); - - assertThat(tickerCaptor.getValue()).isEqualTo(validTicker); - assertThat(startCaptor.getValue()).isBefore(endCaptor.getValue()); - } - - // ===== 헬퍼 메서드들 ===== - private List createMockTrades() { - return List.of( - createTrade(fixedNow.minusSeconds(3), 100.0, 1.0), - createTrade(fixedNow.minusSeconds(2), 150.0, 2.0), - createTrade(fixedNow.minusSeconds(1), 200.0, 3.0) - ); - } - - private Trade createTrade(LocalDateTime tradeTime, Double price, Double size) { - Trade trade = new Trade(); - try { - setField(trade, "tradeTime", tradeTime); - setField(trade, "price", price); - setField(trade, "size", size); - } catch (Exception e) { - throw new RuntimeException("Trade 객체 생성 실패", e); - } - return trade; - } - - private void setField(Object target, String fieldName, Object value) throws Exception { - java.lang.reflect.Field field = Trade.class.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); + assertThat(result).isNotNull(); + assertThat(result.getOpen()).isEqualTo(51500.0); // 시가 새로 설정 + assertThat(result.getHigh()).isEqualTo(51500.0); + assertThat(result.getLow()).isEqualTo(51500.0); + assertThat(result.getClose()).isEqualTo(51500.0); + assertThat(result.getVolume()).isEqualTo(20.0); // 거래량 새로 시작 } } \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataServiceImplTest.java b/src/test/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataServiceImplTest.java new file mode 100644 index 00000000..d2dbe98d --- /dev/null +++ b/src/test/java/com/cleanengine/coin/chart/service/minute/PagingMinuteOhlcDataServiceImplTest.java @@ -0,0 +1,53 @@ +package com.cleanengine.coin.chart.service.minute; + +import com.cleanengine.coin.base.MariaDBAdapterTest; +import com.cleanengine.coin.chart.dto.RealTimeOhlcDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +// 테스트 실행 전 MariaDBAdapterTest 클래스의 @DataJpaTest, @Disabled 어노테이션 주석처리 필요 +@DisplayName("차트 페이징 통합테스트") +@Transactional +@SpringBootTest +public class PagingMinuteOhlcDataServiceImplTest extends MariaDBAdapterTest { + + @Autowired + private PagingMinuteOhlcDataService pagingMinuteOhlcDataService; + + @DisplayName("페이징을 통해 차트 OHLC를 정상적으로 가져온다.") + @Test + @Sql("classpath:db/chart/paging_minute_ohlc_data.sql") + public void getMinuteOhlcData() { + // given + String ticker = "TRUMP"; + int count = 1; + int interval = 1; + LocalDateTime from = LocalDateTime.of(2025, 6, 20, 12, 3, 0); + + // when + List ohlcData = pagingMinuteOhlcDataService.getMinuteOhlcData(ticker, count, interval, from.minusMinutes(1)); + + // then + assertNotNull(ohlcData); + assertEquals(1, ohlcData.size()); + + RealTimeOhlcDto resultDto = ohlcData.getFirst(); + assertEquals("TRUMP", resultDto.getTicker()); + assertEquals(LocalDateTime.of(2025, 6, 20, 12, 2, 0), resultDto.getTimestamp()); + assertEquals(109500.0, resultDto.getOpen()); + assertEquals(110500.0, resultDto.getHigh()); + assertEquals(109300.0, resultDto.getLow()); + assertEquals(110000.0, resultDto.getClose()); + assertEquals(12.541453, resultDto.getVolume(), 0.000001); + } + +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java index 57fde90d..c005bf16 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java +++ b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java @@ -12,7 +12,6 @@ import com.cleanengine.coin.realitybot.parser.TickParserTest; import com.cleanengine.coin.realitybot.service.PlatformVWAPServiceTest; import com.cleanengine.coin.realitybot.service.TickServiceManagerTest; -import com.cleanengine.coin.realitybot.service.VWAPerrorInJectionSchedulerTest; import com.cleanengine.coin.realitybot.vo.DeviationPricePolicyTest; import com.cleanengine.coin.realitybot.vo.OrderPricePolicyTest; import com.cleanengine.coin.realitybot.vo.UnitPricePolicyTest; @@ -34,7 +33,6 @@ TicksTest.class, TickParserTest.class, OpeningPriceParserTest.class, - VWAPerrorInJectionSchedulerTest.class, OrderPricePolicyTest.class, DeviationPricePolicyTest.class }) diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java index 1eaca43f..92ea3bce 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java @@ -3,14 +3,19 @@ import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; import com.cleanengine.coin.order.domain.Asset; import com.cleanengine.coin.realitybot.domain.APIVWAPState; +import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder; import com.cleanengine.coin.realitybot.dto.Ticks; import com.cleanengine.coin.realitybot.parser.TickParser; import com.cleanengine.coin.realitybot.service.OrderGenerateService; import com.cleanengine.coin.realitybot.service.TickServiceManager; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; @@ -20,6 +25,7 @@ @ExtendWith(MockitoExtension.class) public class ApiSchedulerTest { + @Spy @InjectMocks private ApiScheduler apiScheduler; @@ -30,16 +36,32 @@ public class ApiSchedulerTest { @Mock private OrderGenerateService orderGenerateService; @Mock - APIVWAPState apiVWAPState; + private APIVWAPState apiVWAPState; @Mock private TickServiceManager tickServiceManager; @Mock private AssetRepository assetRepository; + @Mock + private MeterRegistry meterRegistry; + @Mock + private Timer timer; + @Mock + private VWAPMetricsRecorder recorder; - + @BeforeEach + void setUp() { + when(meterRegistry.timer(anyString())).thenReturn(timer); + // Timer의 record 메서드가 람다를 실행하도록 설정 + doAnswer(invocation -> { + Runnable runnable = invocation.getArgument(0); + runnable.run(); + return null; + }).when(timer).record(any(Runnable.class)); + } @Test void marketAllRequestCallsAllTickers() throws InterruptedException { + // Given List assets = List.of( new Asset("BTC", "비트코인", null), new Asset("TRUMP", "트럼프", null), @@ -53,26 +75,23 @@ void marketAllRequestCallsAllTickers() throws InterruptedException { new Asset("WLD", "월드코인", null) ); List testTicks = List.of( - new Ticks("BTC","2025-06-01","11:30:25","2025-06-01T11:30:25.123Z",95730000.0f,0.0082,95000000.0f,730000.0,"ASK",100001L), - new Ticks("ETH","2025-06-01","11:31:10","2025-06-01T11:31:10.456Z",4850000.0f,1.25,4800000.0f,50000.0,"BID",100002L), - new Ticks("DOGE","2025-06-01","11:32:45","2025-06-01T11:32:45.789Z",185.5f,9500.0,180.0f,5.5,"ASK", 100003L)); - Ticks ticks = new Ticks("BTC","2025-06-01","11:30:25","2025-06-01T11:30:25.123Z",95730000.0f,0.0082,95000000.0f,730000.0,"ASK",100001L); - //given + new Ticks("BTC", "2025-06-01", "11:30:25", "2025-06-01T11:30:25.123Z", 95730000.0f, 0.0082, 95000000.0f, 730000.0, "ASK", 100001L), + new Ticks("ETH", "2025-06-01", "11:31:10", "2025-06-01T11:31:10.456Z", 4850000.0f, 1.25, 4800000.0f, 50000.0, "BID", 100002L), + new Ticks("DOGE", "2025-06-01", "11:32:45", "2025-06-01T11:32:45.789Z", 185.5f, 9500.0, 180.0f, 5.5, "ASK", 100003L) + ); + when(assetRepository.findAll()).thenReturn(assets); when(apiClient.get(anyString())).thenReturn("[{data:...}]"); when(tickParser.parseGson(anyString())).thenReturn(testTicks); when(tickServiceManager.getService(anyString())).thenReturn(apiVWAPState); doNothing().when(apiVWAPState).addTick(any()); - System.out.println(assets.size()); - //when + doNothing().when(recorder).recordApiVwap(anyString(),anyDouble()); + + // When apiScheduler.MarketAllRequest(); - //then -// verify(apiScheduler,times(assets.size())).MarketDataRequest(anyString()); + // Then + verify(apiScheduler, times(assets.size())).MarketDataRequest(anyString()); verify(orderGenerateService,times(assets.size())).generateOrder(anyString(),anyDouble(),anyDouble()); } - - @Test - void marketDataRequest() { - } } \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java index 58105cc7..f3ea52c1 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java @@ -2,6 +2,7 @@ import com.cleanengine.coin.realitybot.domain.PlatformVWAPState; import com.cleanengine.coin.trade.entity.Trade; +import com.cleanengine.coin.trade.repository.TradeRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +23,9 @@ public class PlatformVWAPServiceTest { @InjectMocks private PlatformVWAPService platformVWAPService; + @Mock + private TradeRepository tradeRepository; + @Mock private PlatformVWAPState platformVwapState; @@ -36,6 +40,7 @@ void testCalculateVWAPLessThan10Trades() { new Trade(3, "BTC", LocalDateTime.now(), 2, 1, 12000.0, 10.0) // 120000 );//이게 적용되면 10000원대 double apiVWAP = 1000.0; //0.1%의 보정값 + when(tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker)).thenReturn(trades); //when double result = platformVWAPService.calculateVWAPbyTrades(ticker, apiVWAP); @@ -64,8 +69,10 @@ void testCalculateVWAPMoreThan10Trades() { new Trade(11, "BTC", LocalDateTime.now(), 2, 1, 20000.0, 10.0) // 200000 ); double apiVWAP = 1000.0; + when(tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker)).thenReturn(trades); when(platformVwapState.getVWAP()).thenReturn(15000.0); platformVWAPService.vwapMap.put(ticker, platformVwapState); + //when double result = platformVWAPService.calculateVWAPbyTrades(ticker, apiVWAP); diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java deleted file mode 100644 index 6806f901..00000000 --- a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.cleanengine.coin.realitybot.service; - -import com.cleanengine.coin.trade.entity.Trade; -import com.cleanengine.coin.trade.repository.TradeRepository; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; - -import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; -import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; - -@Component -public class VWAPErrorInjector { - private final TradeRepository tradeRepository; - - public VWAPErrorInjector(TradeRepository tradeRepository) { - this.tradeRepository = tradeRepository; - } - - public void injectErrorTrade(){ - Trade fakeTrade = new Trade(); - fakeTrade.setTicker("TRUMP"); - fakeTrade.setBuyUserId(BUY_ORDER_BOT_ID); // 테스트용 유저 ID - fakeTrade.setSellUserId(SELL_ORDER_BOT_ID); // 테스트용 유저 ID - fakeTrade.setPrice(25000.0); // 말도 안되는 고가 (예: 시장 평균이 19,000일 때) -// fakeTrade.setPrice(18900.0); // 말도 안되는 고가 (예: 시장 평균이 19,000일 때) - fakeTrade.setSize(300.0); // 대량 체결 -// fakeTrade.setSize(100.0); // 대량 체결 - fakeTrade.setTradeTime(LocalDateTime.now()); - - tradeRepository.save(fakeTrade); - } -} diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java deleted file mode 100644 index a5c884a8..00000000 --- a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.cleanengine.coin.realitybot.service; - -import com.cleanengine.coin.trade.entity.Trade; -import com.cleanengine.coin.trade.repository.TradeRepository; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -public class VWAPerrorInJectionSchedulerTest { - - @Mock - TradeRepository tradeRepository; - - @InjectMocks - VWAPErrorInjector vwapErrorInjector; - - - @Test - @DisplayName("enableInjection() 호출 전에는 작동 안한다") - void doNotingInjection(){ - vwapErrorInjector.injectErrorTrade(); - verify(tradeRepository,never()).save(any()); - } - - @Test - @DisplayName("호출 후에 fateTrade 삽입") - void injectOnceAfterEnable(){ - vwapErrorInjector.injectErrorTrade(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Trade.class); - verify(tradeRepository,times(1)).save(captor.capture()); - - Trade trade = captor.getValue(); - assertEquals("TRUMP",trade.getTicker()); - } -} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java b/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java new file mode 100644 index 00000000..a89b282e --- /dev/null +++ b/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java @@ -0,0 +1,60 @@ +package com.cleanengine.coin.tool; + +import com.cleanengine.coin.user.login.application.JWTUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +@Profile("dev, it") +public class TokenGenerator { + + @Value("${JWT_SECRET}") + private String secret; + + @Test + public void createToken(){ + JWTUtil jwtUtil = new JWTUtil(secret); + System.out.println(jwtUtil.createJwt(1001, 1000 * 60*60*24*365L)); + System.out.println(jwtUtil.createJwt(1002, 1000 * 60*60*24*365L)); + } + + @Test + public void createTokens(){ + int size = 0; + List tokens = new ArrayList<>(); + + JWTUtil jwtUtil = new JWTUtil(secret); + + for (int i = 1; i<=1000; i++){ + tokens.add(jwtUtil.createJwt(i, 1000 * 60*60*24*365L)); + size++; + } + + ObjectMapper objectMapper = new ObjectMapper(); + + try{ + File outputFile = new File("tokens.json"); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(outputFile, new UserTokens(size, tokens)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static class UserTokens{ + public int size; + public List tokens; + + public UserTokens(int size, List tokens) { + this.size = size; + this.tokens = tokens; + } + } +} diff --git a/src/test/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserServiceTest.java b/src/test/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserServiceTest.java index c2e1f850..83022c15 100644 --- a/src/test/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserServiceTest.java +++ b/src/test/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserServiceTest.java @@ -1,14 +1,15 @@ package com.cleanengine.coin.user.login.application; import com.cleanengine.coin.common.CommonValues; +import com.cleanengine.coin.user.domain.Account; import com.cleanengine.coin.user.domain.OAuth; import com.cleanengine.coin.user.domain.User; import com.cleanengine.coin.user.info.application.AccountService; import com.cleanengine.coin.user.info.application.WalletService; -import com.cleanengine.coin.user.login.infra.CustomOAuth2User; -import com.cleanengine.coin.user.login.infra.UserOAuthDetails; import com.cleanengine.coin.user.info.infra.OAuthRepository; import com.cleanengine.coin.user.info.infra.UserRepository; +import com.cleanengine.coin.user.login.infra.CustomOAuth2User; +import com.cleanengine.coin.user.login.infra.UserOAuthDetails; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -96,6 +97,7 @@ void whenNewUser_thenCreateUserAndOAuth() { user.setId(userId); return user; }); + when(accountService.createNewAccount(userId, CommonValues.INITIAL_USER_CASH)).thenReturn(Account.of(userId, CommonValues.INITIAL_USER_CASH)); // When OAuth2User result = customOAuth2UserService.loadUser(userRequest); diff --git a/src/test/resources/db/chart/paging_minute_ohlc_data.sql b/src/test/resources/db/chart/paging_minute_ohlc_data.sql new file mode 100644 index 00000000..b409b09a --- /dev/null +++ b/src/test/resources/db/chart/paging_minute_ohlc_data.sql @@ -0,0 +1,5 @@ +-- 테스트 실행 전 데이터 정리 +DELETE FROM trade WHERE ticker = 'TRUMP'; + +INSERT INTO trade (ticker, trade_time, price, size, sell_user_id, buy_user_id) VALUES + ('TRUMP', '2025-06-20 10:22:00', 9500, 8.153362, 1, 2),('TRUMP', '2025-06-20 10:22:15', 10500, 9.354606, 1, 2),('TRUMP', '2025-06-20 10:22:30', 9300, 0.384524, 1, 2),('TRUMP', '2025-06-20 10:22:45', 10000, 7.740619, 1, 2),('TRUMP', '2025-06-20 10:23:00', 10500, 1.243979, 1, 2),('TRUMP', '2025-06-20 10:23:15', 11500, 4.256199, 1, 2),('TRUMP', '2025-06-20 10:23:30', 10300, 2.975785, 1, 2),('TRUMP', '2025-06-20 10:23:45', 11000, 0.345685, 1, 2),('TRUMP', '2025-06-20 10:24:00', 11500, 0.954383, 1, 2),('TRUMP', '2025-06-20 10:24:15', 12500, 0.959916, 1, 2),('TRUMP', '2025-06-20 10:24:30', 11300, 4.49533, 1, 2),('TRUMP', '2025-06-20 10:24:45', 12000, 4.940202, 1, 2),('TRUMP', '2025-06-20 10:25:00', 12500, 2.899227, 1, 2),('TRUMP', '2025-06-20 10:25:15', 13500, 5.719963, 1, 2),('TRUMP', '2025-06-20 10:25:30', 12300, 6.139157, 1, 2),('TRUMP', '2025-06-20 10:25:45', 13000, 5.847814, 1, 2),('TRUMP', '2025-06-20 10:26:00', 13500, 1.590967, 1, 2),('TRUMP', '2025-06-20 10:26:15', 14500, 4.05136, 1, 2),('TRUMP', '2025-06-20 10:26:30', 13300, 3.549618, 1, 2),('TRUMP', '2025-06-20 10:26:45', 14000, 7.340675, 1, 2),('TRUMP', '2025-06-20 10:27:00', 14500, 2.096548, 1, 2),('TRUMP', '2025-06-20 10:27:15', 15500, 7.836501, 1, 2),('TRUMP', '2025-06-20 10:27:30', 14300, 9.941319, 1, 2),('TRUMP', '2025-06-20 10:27:45', 15000, 6.225751, 1, 2),('TRUMP', '2025-06-20 10:28:00', 15500, 8.033758, 1, 2),('TRUMP', '2025-06-20 10:28:15', 16500, 4.751001, 1, 2),('TRUMP', '2025-06-20 10:28:30', 15300, 0.334912, 1, 2),('TRUMP', '2025-06-20 10:28:45', 16000, 2.829067, 1, 2),('TRUMP', '2025-06-20 10:29:00', 16500, 8.738852, 1, 2),('TRUMP', '2025-06-20 10:29:15', 17500, 3.756025, 1, 2),('TRUMP', '2025-06-20 10:29:30', 16300, 8.46097, 1, 2),('TRUMP', '2025-06-20 10:29:45', 17000, 3.266099, 1, 2),('TRUMP', '2025-06-20 10:30:00', 17500, 4.062761, 1, 2),('TRUMP', '2025-06-20 10:30:15', 18500, 0.678992, 1, 2),('TRUMP', '2025-06-20 10:30:30', 17300, 2.047525, 1, 2),('TRUMP', '2025-06-20 10:30:45', 18000, 8.508028, 1, 2),('TRUMP', '2025-06-20 10:31:00', 18500, 1.216953, 1, 2),('TRUMP', '2025-06-20 10:31:15', 19500, 0.821937, 1, 2),('TRUMP', '2025-06-20 10:31:30', 18300, 3.601404, 1, 2),('TRUMP', '2025-06-20 10:31:45', 19000, 4.361163, 1, 2),('TRUMP', '2025-06-20 10:32:00', 19500, 4.933111, 1, 2),('TRUMP', '2025-06-20 10:32:15', 20500, 5.506451, 1, 2),('TRUMP', '2025-06-20 10:32:30', 19300, 9.202676, 1, 2),('TRUMP', '2025-06-20 10:32:45', 20000, 5.653409, 1, 2),('TRUMP', '2025-06-20 10:33:00', 20500, 5.683252, 1, 2),('TRUMP', '2025-06-20 10:33:15', 21500, 0.348179, 1, 2),('TRUMP', '2025-06-20 10:33:30', 20300, 0.633755, 1, 2),('TRUMP', '2025-06-20 10:33:45', 21000, 9.088331, 1, 2),('TRUMP', '2025-06-20 10:34:00', 21500, 0.287686, 1, 2),('TRUMP', '2025-06-20 10:34:15', 22500, 6.947126, 1, 2),('TRUMP', '2025-06-20 10:34:30', 21300, 8.892403, 1, 2),('TRUMP', '2025-06-20 10:34:45', 22000, 8.700041, 1, 2),('TRUMP', '2025-06-20 10:35:00', 22500, 1.187726, 1, 2),('TRUMP', '2025-06-20 10:35:15', 23500, 7.512412, 1, 2),('TRUMP', '2025-06-20 10:35:30', 22300, 9.893827, 1, 2),('TRUMP', '2025-06-20 10:35:45', 23000, 1.433184, 1, 2),('TRUMP', '2025-06-20 10:36:00', 23500, 8.082752, 1, 2),('TRUMP', '2025-06-20 10:36:15', 24500, 1.912229, 1, 2),('TRUMP', '2025-06-20 10:36:30', 23300, 5.040846, 1, 2),('TRUMP', '2025-06-20 10:36:45', 24000, 2.975444, 1, 2),('TRUMP', '2025-06-20 10:37:00', 24500, 6.220338, 1, 2),('TRUMP', '2025-06-20 10:37:15', 25500, 7.700525, 1, 2),('TRUMP', '2025-06-20 10:37:30', 24300, 4.631947, 1, 2),('TRUMP', '2025-06-20 10:37:45', 25000, 9.83329, 1, 2),('TRUMP', '2025-06-20 10:38:00', 25500, 9.870799, 1, 2),('TRUMP', '2025-06-20 10:38:15', 26500, 5.443923, 1, 2),('TRUMP', '2025-06-20 10:38:30', 25300, 7.731651, 1, 2),('TRUMP', '2025-06-20 10:38:45', 26000, 4.918671, 1, 2),('TRUMP', '2025-06-20 10:39:00', 26500, 3.779329, 1, 2),('TRUMP', '2025-06-20 10:39:15', 27500, 2.857269, 1, 2),('TRUMP', '2025-06-20 10:39:30', 26300, 9.283234, 1, 2),('TRUMP', '2025-06-20 10:39:45', 27000, 7.857575, 1, 2),('TRUMP', '2025-06-20 10:40:00', 27500, 8.565832, 1, 2),('TRUMP', '2025-06-20 10:40:15', 28500, 5.604584, 1, 2),('TRUMP', '2025-06-20 10:40:30', 27300, 7.412103, 1, 2),('TRUMP', '2025-06-20 10:40:45', 28000, 5.411814, 1, 2),('TRUMP', '2025-06-20 10:41:00', 28500, 2.777743, 1, 2),('TRUMP', '2025-06-20 10:41:15', 29500, 1.552006, 1, 2),('TRUMP', '2025-06-20 10:41:30', 28300, 3.847869, 1, 2),('TRUMP', '2025-06-20 10:41:45', 29000, 8.471987, 1, 2),('TRUMP', '2025-06-20 10:42:00', 29500, 3.469157, 1, 2),('TRUMP', '2025-06-20 10:42:15', 30500, 0.414554, 1, 2),('TRUMP', '2025-06-20 10:42:30', 29300, 4.330753, 1, 2),('TRUMP', '2025-06-20 10:42:45', 30000, 6.570765, 1, 2),('TRUMP', '2025-06-20 10:43:00', 30500, 2.595921, 1, 2),('TRUMP', '2025-06-20 10:43:15', 31500, 7.149546, 1, 2),('TRUMP', '2025-06-20 10:43:30', 30300, 7.434402, 1, 2),('TRUMP', '2025-06-20 10:43:45', 31000, 8.100091, 1, 2),('TRUMP', '2025-06-20 10:44:00', 31500, 1.405644, 1, 2),('TRUMP', '2025-06-20 10:44:15', 32500, 9.467642, 1, 2),('TRUMP', '2025-06-20 10:44:30', 31300, 3.755448, 1, 2),('TRUMP', '2025-06-20 10:44:45', 32000, 3.571819, 1, 2),('TRUMP', '2025-06-20 10:45:00', 32500, 0.946804, 1, 2),('TRUMP', '2025-06-20 10:45:15', 33500, 8.844795, 1, 2),('TRUMP', '2025-06-20 10:45:30', 32300, 7.994338, 1, 2),('TRUMP', '2025-06-20 10:45:45', 33000, 4.205067, 1, 2),('TRUMP', '2025-06-20 10:46:00', 33500, 2.030883, 1, 2),('TRUMP', '2025-06-20 10:46:15', 34500, 0.473501, 1, 2),('TRUMP', '2025-06-20 10:46:30', 33300, 2.548191, 1, 2),('TRUMP', '2025-06-20 10:46:45', 34000, 1.827358, 1, 2),('TRUMP', '2025-06-20 10:47:00', 34500, 1.384758, 1, 2),('TRUMP', '2025-06-20 10:47:15', 35500, 3.781906, 1, 2),('TRUMP', '2025-06-20 10:47:30', 34300, 5.312981, 1, 2),('TRUMP', '2025-06-20 10:47:45', 35000, 1.744585, 1, 2),('TRUMP', '2025-06-20 10:48:00', 35500, 9.488945, 1, 2),('TRUMP', '2025-06-20 10:48:15', 36500, 6.468489, 1, 2),('TRUMP', '2025-06-20 10:48:30', 35300, 9.661418, 1, 2),('TRUMP', '2025-06-20 10:48:45', 36000, 4.31013, 1, 2),('TRUMP', '2025-06-20 10:49:00', 36500, 4.157009, 1, 2),('TRUMP', '2025-06-20 10:49:15', 37500, 2.049607, 1, 2),('TRUMP', '2025-06-20 10:49:30', 36300, 4.665033, 1, 2),('TRUMP', '2025-06-20 10:49:45', 37000, 4.524928, 1, 2),('TRUMP', '2025-06-20 10:50:00', 37500, 4.72518, 1, 2),('TRUMP', '2025-06-20 10:50:15', 38500, 6.472951, 1, 2),('TRUMP', '2025-06-20 10:50:30', 37300, 2.306401, 1, 2),('TRUMP', '2025-06-20 10:50:45', 38000, 1.073369, 1, 2),('TRUMP', '2025-06-20 10:51:00', 38500, 7.014094, 1, 2),('TRUMP', '2025-06-20 10:51:15', 39500, 4.507889, 1, 2),('TRUMP', '2025-06-20 10:51:30', 38300, 7.196793, 1, 2),('TRUMP', '2025-06-20 10:51:45', 39000, 6.235912, 1, 2),('TRUMP', '2025-06-20 10:52:00', 39500, 2.762223, 1, 2),('TRUMP', '2025-06-20 10:52:15', 40500, 3.196428, 1, 2),('TRUMP', '2025-06-20 10:52:30', 39300, 6.596248, 1, 2),('TRUMP', '2025-06-20 10:52:45', 40000, 6.322872, 1, 2),('TRUMP', '2025-06-20 10:53:00', 40500, 1.647339, 1, 2),('TRUMP', '2025-06-20 10:53:15', 41500, 3.348769, 1, 2),('TRUMP', '2025-06-20 10:53:30', 40300, 6.329893, 1, 2),('TRUMP', '2025-06-20 10:53:45', 41000, 2.902979, 1, 2),('TRUMP', '2025-06-20 10:54:00', 41500, 9.555928, 1, 2),('TRUMP', '2025-06-20 10:54:15', 42500, 2.799704, 1, 2),('TRUMP', '2025-06-20 10:54:30', 41300, 1.01994, 1, 2),('TRUMP', '2025-06-20 10:54:45', 42000, 9.762633, 1, 2),('TRUMP', '2025-06-20 10:55:00', 42500, 8.366426, 1, 2),('TRUMP', '2025-06-20 10:55:15', 43500, 9.496634, 1, 2),('TRUMP', '2025-06-20 10:55:30', 42300, 2.10383, 1, 2),('TRUMP', '2025-06-20 10:55:45', 43000, 1.145814, 1, 2),('TRUMP', '2025-06-20 10:56:00', 43500, 5.732417, 1, 2),('TRUMP', '2025-06-20 10:56:15', 44500, 0.579136, 1, 2),('TRUMP', '2025-06-20 10:56:30', 43300, 0.208318, 1, 2),('TRUMP', '2025-06-20 10:56:45', 44000, 5.664976, 1, 2),('TRUMP', '2025-06-20 10:57:00', 44500, 2.771306, 1, 2),('TRUMP', '2025-06-20 10:57:15', 45500, 4.244259, 1, 2),('TRUMP', '2025-06-20 10:57:30', 44300, 6.546404, 1, 2),('TRUMP', '2025-06-20 10:57:45', 45000, 5.717042, 1, 2),('TRUMP', '2025-06-20 10:58:00', 45500, 1.116311, 1, 2),('TRUMP', '2025-06-20 10:58:15', 46500, 4.352062, 1, 2),('TRUMP', '2025-06-20 10:58:30', 45300, 0.325469, 1, 2),('TRUMP', '2025-06-20 10:58:45', 46000, 5.603459, 1, 2),('TRUMP', '2025-06-20 10:59:00', 46500, 3.426694, 1, 2),('TRUMP', '2025-06-20 10:59:15', 47500, 5.961295, 1, 2),('TRUMP', '2025-06-20 10:59:30', 46300, 7.16058, 1, 2),('TRUMP', '2025-06-20 10:59:45', 47000, 3.306473, 1, 2),('TRUMP', '2025-06-20 11:00:00', 47500, 6.05194, 1, 2),('TRUMP', '2025-06-20 11:00:15', 48500, 9.856735, 1, 2),('TRUMP', '2025-06-20 11:00:30', 47300, 0.40779, 1, 2),('TRUMP', '2025-06-20 11:00:45', 48000, 0.527618, 1, 2),('TRUMP', '2025-06-20 11:01:00', 48500, 8.444469, 1, 2),('TRUMP', '2025-06-20 11:01:15', 49500, 5.867208, 1, 2),('TRUMP', '2025-06-20 11:01:30', 48300, 2.404892, 1, 2),('TRUMP', '2025-06-20 11:01:45', 49000, 3.600609, 1, 2),('TRUMP', '2025-06-20 11:02:00', 49500, 7.152466, 1, 2),('TRUMP', '2025-06-20 11:02:15', 50500, 2.219198, 1, 2),('TRUMP', '2025-06-20 11:02:30', 49300, 2.83873, 1, 2),('TRUMP', '2025-06-20 11:02:45', 50000, 7.50575, 1, 2),('TRUMP', '2025-06-20 11:03:00', 50500, 1.582533, 1, 2),('TRUMP', '2025-06-20 11:03:15', 51500, 9.547496, 1, 2),('TRUMP', '2025-06-20 11:03:30', 50300, 3.183479, 1, 2),('TRUMP', '2025-06-20 11:03:45', 51000, 8.71478, 1, 2),('TRUMP', '2025-06-20 11:04:00', 51500, 5.078676, 1, 2),('TRUMP', '2025-06-20 11:04:15', 52500, 8.132647, 1, 2),('TRUMP', '2025-06-20 11:04:30', 51300, 0.907574, 1, 2),('TRUMP', '2025-06-20 11:04:45', 52000, 5.958444, 1, 2),('TRUMP', '2025-06-20 11:05:00', 52500, 2.22762, 1, 2),('TRUMP', '2025-06-20 11:05:15', 53500, 6.487159, 1, 2),('TRUMP', '2025-06-20 11:05:30', 52300, 5.265468, 1, 2),('TRUMP', '2025-06-20 11:05:45', 53000, 3.446569, 1, 2),('TRUMP', '2025-06-20 11:06:00', 53500, 4.431763, 1, 2),('TRUMP', '2025-06-20 11:06:15', 54500, 3.187072, 1, 2),('TRUMP', '2025-06-20 11:06:30', 53300, 8.931232, 1, 2),('TRUMP', '2025-06-20 11:06:45', 54000, 1.939293, 1, 2),('TRUMP', '2025-06-20 11:07:00', 54500, 8.645677, 1, 2),('TRUMP', '2025-06-20 11:07:15', 55500, 0.950108, 1, 2),('TRUMP', '2025-06-20 11:07:30', 54300, 0.718439, 1, 2),('TRUMP', '2025-06-20 11:07:45', 55000, 5.290918, 1, 2),('TRUMP', '2025-06-20 11:08:00', 55500, 1.888176, 1, 2),('TRUMP', '2025-06-20 11:08:15', 56500, 6.184991, 1, 2),('TRUMP', '2025-06-20 11:08:30', 55300, 8.723511, 1, 2),('TRUMP', '2025-06-20 11:08:45', 56000, 9.318781, 1, 2),('TRUMP', '2025-06-20 11:09:00', 56500, 4.989659, 1, 2),('TRUMP', '2025-06-20 11:09:15', 57500, 7.510518, 1, 2),('TRUMP', '2025-06-20 11:09:30', 56300, 4.829685, 1, 2),('TRUMP', '2025-06-20 11:09:45', 57000, 6.100732, 1, 2),('TRUMP', '2025-06-20 11:10:00', 57500, 1.91546, 1, 2),('TRUMP', '2025-06-20 11:10:15', 58500, 9.292318, 1, 2),('TRUMP', '2025-06-20 11:10:30', 57300, 9.936838, 1, 2),('TRUMP', '2025-06-20 11:10:45', 58000, 0.984307, 1, 2),('TRUMP', '2025-06-20 11:11:00', 58500, 5.469901, 1, 2),('TRUMP', '2025-06-20 11:11:15', 59500, 7.8217, 1, 2),('TRUMP', '2025-06-20 11:11:30', 58300, 2.437692, 1, 2),('TRUMP', '2025-06-20 11:11:45', 59000, 2.597637, 1, 2),('TRUMP', '2025-06-20 11:12:00', 59500, 3.842048, 1, 2),('TRUMP', '2025-06-20 11:12:15', 60500, 0.45384, 1, 2),('TRUMP', '2025-06-20 11:12:30', 59300, 4.043604, 1, 2),('TRUMP', '2025-06-20 11:12:45', 60000, 4.041273, 1, 2),('TRUMP', '2025-06-20 11:13:00', 60500, 9.684228, 1, 2),('TRUMP', '2025-06-20 11:13:15', 61500, 6.732698, 1, 2),('TRUMP', '2025-06-20 11:13:30', 60300, 4.436458, 1, 2),('TRUMP', '2025-06-20 11:13:45', 61000, 9.984067, 1, 2),('TRUMP', '2025-06-20 11:14:00', 61500, 6.072332, 1, 2),('TRUMP', '2025-06-20 11:14:15', 62500, 5.534948, 1, 2),('TRUMP', '2025-06-20 11:14:30', 61300, 3.872439, 1, 2),('TRUMP', '2025-06-20 11:14:45', 62000, 0.248097, 1, 2),('TRUMP', '2025-06-20 11:15:00', 62500, 7.229842, 1, 2),('TRUMP', '2025-06-20 11:15:15', 63500, 3.125711, 1, 2),('TRUMP', '2025-06-20 11:15:30', 62300, 4.463964, 1, 2),('TRUMP', '2025-06-20 11:15:45', 63000, 8.487688, 1, 2),('TRUMP', '2025-06-20 11:16:00', 63500, 5.968998, 1, 2),('TRUMP', '2025-06-20 11:16:15', 64500, 5.078699, 1, 2),('TRUMP', '2025-06-20 11:16:30', 63300, 5.102054, 1, 2),('TRUMP', '2025-06-20 11:16:45', 64000, 5.643304, 1, 2),('TRUMP', '2025-06-20 11:17:00', 64500, 3.68429, 1, 2),('TRUMP', '2025-06-20 11:17:15', 65500, 8.467265, 1, 2),('TRUMP', '2025-06-20 11:17:30', 64300, 6.216971, 1, 2),('TRUMP', '2025-06-20 11:17:45', 65000, 0.492547, 1, 2),('TRUMP', '2025-06-20 11:18:00', 65500, 7.833614, 1, 2),('TRUMP', '2025-06-20 11:18:15', 66500, 2.736748, 1, 2),('TRUMP', '2025-06-20 11:18:30', 65300, 5.193063, 1, 2),('TRUMP', '2025-06-20 11:18:45', 66000, 3.976855, 1, 2),('TRUMP', '2025-06-20 11:19:00', 66500, 0.396889, 1, 2),('TRUMP', '2025-06-20 11:19:15', 67500, 7.366372, 1, 2),('TRUMP', '2025-06-20 11:19:30', 66300, 5.727963, 1, 2),('TRUMP', '2025-06-20 11:19:45', 67000, 4.855477, 1, 2),('TRUMP', '2025-06-20 11:20:00', 67500, 0.952008, 1, 2),('TRUMP', '2025-06-20 11:20:15', 68500, 9.047074, 1, 2),('TRUMP', '2025-06-20 11:20:30', 67300, 0.085027, 1, 2),('TRUMP', '2025-06-20 11:20:45', 68000, 1.393916, 1, 2),('TRUMP', '2025-06-20 11:21:00', 68500, 0.766178, 1, 2),('TRUMP', '2025-06-20 11:21:15', 69500, 8.684129, 1, 2),('TRUMP', '2025-06-20 11:21:30', 68300, 9.148265, 1, 2),('TRUMP', '2025-06-20 11:21:45', 69000, 7.422491, 1, 2),('TRUMP', '2025-06-20 11:22:00', 69500, 7.31852, 1, 2),('TRUMP', '2025-06-20 11:22:15', 70500, 7.28414, 1, 2),('TRUMP', '2025-06-20 11:22:30', 69300, 7.470014, 1, 2),('TRUMP', '2025-06-20 11:22:45', 70000, 1.033391, 1, 2),('TRUMP', '2025-06-20 11:23:00', 70500, 8.897752, 1, 2),('TRUMP', '2025-06-20 11:23:15', 71500, 5.477421, 1, 2),('TRUMP', '2025-06-20 11:23:30', 70300, 9.250108, 1, 2),('TRUMP', '2025-06-20 11:23:45', 71000, 1.43142, 1, 2),('TRUMP', '2025-06-20 11:24:00', 71500, 4.133991, 1, 2),('TRUMP', '2025-06-20 11:24:15', 72500, 0.199389, 1, 2),('TRUMP', '2025-06-20 11:24:30', 71300, 1.243383, 1, 2),('TRUMP', '2025-06-20 11:24:45', 72000, 8.128695, 1, 2),('TRUMP', '2025-06-20 11:25:00', 72500, 6.692316, 1, 2),('TRUMP', '2025-06-20 11:25:15', 73500, 7.809474, 1, 2),('TRUMP', '2025-06-20 11:25:30', 72300, 7.60761, 1, 2),('TRUMP', '2025-06-20 11:25:45', 73000, 4.817694, 1, 2),('TRUMP', '2025-06-20 11:26:00', 73500, 9.541947, 1, 2),('TRUMP', '2025-06-20 11:26:15', 74500, 2.753095, 1, 2),('TRUMP', '2025-06-20 11:26:30', 73300, 3.564174, 1, 2),('TRUMP', '2025-06-20 11:26:45', 74000, 9.544686, 1, 2),('TRUMP', '2025-06-20 11:27:00', 74500, 3.000097, 1, 2),('TRUMP', '2025-06-20 11:27:15', 75500, 0.683207, 1, 2),('TRUMP', '2025-06-20 11:27:30', 74300, 4.389624, 1, 2),('TRUMP', '2025-06-20 11:27:45', 75000, 5.52884, 1, 2),('TRUMP', '2025-06-20 11:28:00', 75500, 7.193088, 1, 2),('TRUMP', '2025-06-20 11:28:15', 76500, 7.593824, 1, 2),('TRUMP', '2025-06-20 11:28:30', 75300, 2.919355, 1, 2),('TRUMP', '2025-06-20 11:28:45', 76000, 9.208124, 1, 2),('TRUMP', '2025-06-20 11:29:00', 76500, 2.696081, 1, 2),('TRUMP', '2025-06-20 11:29:15', 77500, 0.23134, 1, 2),('TRUMP', '2025-06-20 11:29:30', 76300, 1.878147, 1, 2),('TRUMP', '2025-06-20 11:29:45', 77000, 0.406767, 1, 2),('TRUMP', '2025-06-20 11:30:00', 77500, 4.447307, 1, 2),('TRUMP', '2025-06-20 11:30:15', 78500, 0.665482, 1, 2),('TRUMP', '2025-06-20 11:30:30', 77300, 2.566807, 1, 2),('TRUMP', '2025-06-20 11:30:45', 78000, 1.370328, 1, 2),('TRUMP', '2025-06-20 11:31:00', 78500, 6.861776, 1, 2),('TRUMP', '2025-06-20 11:31:15', 79500, 0.648956, 1, 2),('TRUMP', '2025-06-20 11:31:30', 78300, 7.008633, 1, 2),('TRUMP', '2025-06-20 11:31:45', 79000, 0.605771, 1, 2),('TRUMP', '2025-06-20 11:32:00', 79500, 0.507911, 1, 2),('TRUMP', '2025-06-20 11:32:15', 80500, 9.409231, 1, 2),('TRUMP', '2025-06-20 11:32:30', 79300, 4.204007, 1, 2),('TRUMP', '2025-06-20 11:32:45', 80000, 7.903682, 1, 2),('TRUMP', '2025-06-20 11:33:00', 80500, 2.953276, 1, 2),('TRUMP', '2025-06-20 11:33:15', 81500, 7.501662, 1, 2),('TRUMP', '2025-06-20 11:33:30', 80300, 9.350852, 1, 2),('TRUMP', '2025-06-20 11:33:45', 81000, 4.424719, 1, 2),('TRUMP', '2025-06-20 11:34:00', 81500, 2.177221, 1, 2),('TRUMP', '2025-06-20 11:34:15', 82500, 8.182877, 1, 2),('TRUMP', '2025-06-20 11:34:30', 81300, 7.966547, 1, 2),('TRUMP', '2025-06-20 11:34:45', 82000, 9.432963, 1, 2),('TRUMP', '2025-06-20 11:35:00', 82500, 6.333328, 1, 2),('TRUMP', '2025-06-20 11:35:15', 83500, 9.676336, 1, 2),('TRUMP', '2025-06-20 11:35:30', 82300, 2.606207, 1, 2),('TRUMP', '2025-06-20 11:35:45', 83000, 2.775059, 1, 2),('TRUMP', '2025-06-20 11:36:00', 83500, 0.759764, 1, 2),('TRUMP', '2025-06-20 11:36:15', 84500, 7.04231, 1, 2),('TRUMP', '2025-06-20 11:36:30', 83300, 7.461589, 1, 2),('TRUMP', '2025-06-20 11:36:45', 84000, 2.153273, 1, 2),('TRUMP', '2025-06-20 11:37:00', 84500, 6.881254, 1, 2),('TRUMP', '2025-06-20 11:37:15', 85500, 2.822749, 1, 2),('TRUMP', '2025-06-20 11:37:30', 84300, 8.397513, 1, 2),('TRUMP', '2025-06-20 11:37:45', 85000, 1.612214, 1, 2),('TRUMP', '2025-06-20 11:38:00', 85500, 0.392017, 1, 2),('TRUMP', '2025-06-20 11:38:15', 86500, 1.315367, 1, 2),('TRUMP', '2025-06-20 11:38:30', 85300, 2.501414, 1, 2),('TRUMP', '2025-06-20 11:38:45', 86000, 5.130304, 1, 2),('TRUMP', '2025-06-20 11:39:00', 86500, 0.549436, 1, 2),('TRUMP', '2025-06-20 11:39:15', 87500, 7.140674, 1, 2),('TRUMP', '2025-06-20 11:39:30', 86300, 8.836336, 1, 2),('TRUMP', '2025-06-20 11:39:45', 87000, 9.378574, 1, 2),('TRUMP', '2025-06-20 11:40:00', 87500, 5.29662, 1, 2),('TRUMP', '2025-06-20 11:40:15', 88500, 4.652293, 1, 2),('TRUMP', '2025-06-20 11:40:30', 87300, 0.983418, 1, 2),('TRUMP', '2025-06-20 11:40:45', 88000, 4.755681, 1, 2),('TRUMP', '2025-06-20 11:41:00', 88500, 9.373903, 1, 2),('TRUMP', '2025-06-20 11:41:15', 89500, 8.581915, 1, 2),('TRUMP', '2025-06-20 11:41:30', 88300, 1.77968, 1, 2),('TRUMP', '2025-06-20 11:41:45', 89000, 9.058383, 1, 2),('TRUMP', '2025-06-20 11:42:00', 89500, 1.892553, 1, 2),('TRUMP', '2025-06-20 11:42:15', 90500, 8.767691, 1, 2),('TRUMP', '2025-06-20 11:42:30', 89300, 3.668953, 1, 2),('TRUMP', '2025-06-20 11:42:45', 90000, 3.199901, 1, 2),('TRUMP', '2025-06-20 11:43:00', 90500, 9.938539, 1, 2),('TRUMP', '2025-06-20 11:43:15', 91500, 7.643165, 1, 2),('TRUMP', '2025-06-20 11:43:30', 90300, 8.80233, 1, 2),('TRUMP', '2025-06-20 11:43:45', 91000, 8.547501, 1, 2),('TRUMP', '2025-06-20 11:44:00', 91500, 7.745982, 1, 2),('TRUMP', '2025-06-20 11:44:15', 92500, 0.429536, 1, 2),('TRUMP', '2025-06-20 11:44:30', 91300, 6.897348, 1, 2),('TRUMP', '2025-06-20 11:44:45', 92000, 3.26145, 1, 2),('TRUMP', '2025-06-20 11:45:00', 92500, 4.700014, 1, 2),('TRUMP', '2025-06-20 11:45:15', 93500, 5.340909, 1, 2),('TRUMP', '2025-06-20 11:45:30', 92300, 5.960919, 1, 2),('TRUMP', '2025-06-20 11:45:45', 93000, 7.632433, 1, 2),('TRUMP', '2025-06-20 11:46:00', 93500, 8.37229, 1, 2),('TRUMP', '2025-06-20 11:46:15', 94500, 5.693355, 1, 2),('TRUMP', '2025-06-20 11:46:30', 93300, 9.701897, 1, 2),('TRUMP', '2025-06-20 11:46:45', 94000, 0.704855, 1, 2),('TRUMP', '2025-06-20 11:47:00', 94500, 2.266811, 1, 2),('TRUMP', '2025-06-20 11:47:15', 95500, 0.216896, 1, 2),('TRUMP', '2025-06-20 11:47:30', 94300, 4.949499, 1, 2),('TRUMP', '2025-06-20 11:47:45', 95000, 7.01654, 1, 2),('TRUMP', '2025-06-20 11:48:00', 95500, 1.204877, 1, 2),('TRUMP', '2025-06-20 11:48:15', 96500, 4.594884, 1, 2),('TRUMP', '2025-06-20 11:48:30', 95300, 4.120086, 1, 2),('TRUMP', '2025-06-20 11:48:45', 96000, 4.742081, 1, 2),('TRUMP', '2025-06-20 11:49:00', 96500, 1.348995, 1, 2),('TRUMP', '2025-06-20 11:49:15', 97500, 4.555622, 1, 2),('TRUMP', '2025-06-20 11:49:30', 96300, 8.347072, 1, 2),('TRUMP', '2025-06-20 11:49:45', 97000, 8.529408, 1, 2),('TRUMP', '2025-06-20 11:50:00', 97500, 1.74083, 1, 2),('TRUMP', '2025-06-20 11:50:15', 98500, 3.387903, 1, 2),('TRUMP', '2025-06-20 11:50:30', 97300, 2.302651, 1, 2),('TRUMP', '2025-06-20 11:50:45', 98000, 4.110105, 1, 2),('TRUMP', '2025-06-20 11:51:00', 98500, 2.630555, 1, 2),('TRUMP', '2025-06-20 11:51:15', 99500, 8.4047, 1, 2),('TRUMP', '2025-06-20 11:51:30', 98300, 2.729595, 1, 2),('TRUMP', '2025-06-20 11:51:45', 99000, 9.42922, 1, 2),('TRUMP', '2025-06-20 11:52:00', 99500, 4.377488, 1, 2),('TRUMP', '2025-06-20 11:52:15', 100500, 9.751386, 1, 2),('TRUMP', '2025-06-20 11:52:30', 99300, 5.164194, 1, 2),('TRUMP', '2025-06-20 11:52:45', 100000, 2.635391, 1, 2),('TRUMP', '2025-06-20 11:53:00', 100500, 4.013014, 1, 2),('TRUMP', '2025-06-20 11:53:15', 101500, 4.731469, 1, 2),('TRUMP', '2025-06-20 11:53:30', 100300, 7.432913, 1, 2),('TRUMP', '2025-06-20 11:53:45', 101000, 8.695532, 1, 2),('TRUMP', '2025-06-20 11:54:00', 101500, 4.343483, 1, 2),('TRUMP', '2025-06-20 11:54:15', 102500, 5.724327, 1, 2),('TRUMP', '2025-06-20 11:54:30', 101300, 2.461119, 1, 2),('TRUMP', '2025-06-20 11:54:45', 102000, 4.693942, 1, 2),('TRUMP', '2025-06-20 11:55:00', 102500, 7.751275, 1, 2),('TRUMP', '2025-06-20 11:55:15', 103500, 4.800228, 1, 2),('TRUMP', '2025-06-20 11:55:30', 102300, 2.94959, 1, 2),('TRUMP', '2025-06-20 11:55:45', 103000, 9.782406, 1, 2),('TRUMP', '2025-06-20 11:56:00', 103500, 2.560125, 1, 2),('TRUMP', '2025-06-20 11:56:15', 104500, 5.98593, 1, 2),('TRUMP', '2025-06-20 11:56:30', 103300, 9.635333, 1, 2),('TRUMP', '2025-06-20 11:56:45', 104000, 7.223809, 1, 2),('TRUMP', '2025-06-20 11:57:00', 104500, 4.185954, 1, 2),('TRUMP', '2025-06-20 11:57:15', 105500, 0.988288, 1, 2),('TRUMP', '2025-06-20 11:57:30', 104300, 7.603267, 1, 2),('TRUMP', '2025-06-20 11:57:45', 105000, 9.729893, 1, 2),('TRUMP', '2025-06-20 11:58:00', 105500, 8.540655, 1, 2),('TRUMP', '2025-06-20 11:58:15', 106500, 4.084503, 1, 2),('TRUMP', '2025-06-20 11:58:30', 105300, 7.140806, 1, 2),('TRUMP', '2025-06-20 11:58:45', 106000, 0.045304, 1, 2),('TRUMP', '2025-06-20 11:59:00', 106500, 3.90966, 1, 2),('TRUMP', '2025-06-20 11:59:15', 107500, 7.329552, 1, 2),('TRUMP', '2025-06-20 11:59:30', 106300, 2.955384, 1, 2),('TRUMP', '2025-06-20 11:59:45', 107000, 8.157939, 1, 2),('TRUMP', '2025-06-20 12:00:00', 107500, 1.92899, 1, 2),('TRUMP', '2025-06-20 12:00:15', 108500, 1.803083, 1, 2),('TRUMP', '2025-06-20 12:00:30', 107300, 9.282569, 1, 2),('TRUMP', '2025-06-20 12:00:45', 108000, 2.242956, 1, 2),('TRUMP', '2025-06-20 12:01:00', 108500, 3.440809, 1, 2),('TRUMP', '2025-06-20 12:01:15', 109500, 8.508542, 1, 2),('TRUMP', '2025-06-20 12:01:30', 108300, 9.816551, 1, 2),('TRUMP', '2025-06-20 12:01:45', 109000, 1.729495, 1, 2),('TRUMP', '2025-06-20 12:02:00', 109500, 1.327527, 1, 2),('TRUMP', '2025-06-20 12:02:15', 110500, 0.480537, 1, 2),('TRUMP', '2025-06-20 12:02:30', 109300, 5.50016, 1, 2),('TRUMP', '2025-06-20 12:02:45', 110000, 5.233229, 1, 2),('TRUMP', '2025-06-20 12:03:00', 110500, 9.01953, 1, 2),('TRUMP', '2025-06-20 12:03:15', 111500, 0.517005, 1, 2),('TRUMP', '2025-06-20 12:03:30', 110300, 6.135884, 1, 2),('TRUMP', '2025-06-20 12:03:45', 111000, 0.554554, 1, 2),('TRUMP', '2025-06-20 12:04:00', 111500, 9.766444, 1, 2),('TRUMP', '2025-06-20 12:04:15', 112500, 2.83883, 1, 2),('TRUMP', '2025-06-20 12:04:30', 111300, 5.414774, 1, 2),('TRUMP', '2025-06-20 12:04:45', 112000, 7.862254, 1, 2),('TRUMP', '2025-06-20 12:05:00', 112500, 7.819496, 1, 2),('TRUMP', '2025-06-20 12:05:15', 113500, 4.315727, 1, 2),('TRUMP', '2025-06-20 12:05:30', 112300, 8.050098, 1, 2),('TRUMP', '2025-06-20 12:05:45', 113000, 0.411363, 1, 2),('TRUMP', '2025-06-20 12:06:00', 113500, 9.705154, 1, 2),('TRUMP', '2025-06-20 12:06:15', 114500, 3.651394, 1, 2),('TRUMP', '2025-06-20 12:06:30', 113300, 3.038723, 1, 2),('TRUMP', '2025-06-20 12:06:45', 114000, 0.478121, 1, 2),('TRUMP', '2025-06-20 12:07:00', 114500, 7.623657, 1, 2),('TRUMP', '2025-06-20 12:07:15', 115500, 8.930488, 1, 2),('TRUMP', '2025-06-20 12:07:30', 114300, 3.49409, 1, 2),('TRUMP', '2025-06-20 12:07:45', 115000, 5.750903, 1, 2),('TRUMP', '2025-06-20 12:08:00', 115500, 5.713631, 1, 2),('TRUMP', '2025-06-20 12:08:15', 116500, 2.022614, 1, 2),('TRUMP', '2025-06-20 12:08:30', 115300, 9.442899, 1, 2),('TRUMP', '2025-06-20 12:08:45', 116000, 4.6167, 1, 2),('TRUMP', '2025-06-20 12:09:00', 116500, 8.751747, 1, 2),('TRUMP', '2025-06-20 12:09:15', 117500, 2.77959, 1, 2),('TRUMP', '2025-06-20 12:09:30', 116300, 8.816881, 1, 2),('TRUMP', '2025-06-20 12:09:45', 117000, 5.29689, 1, 2),('TRUMP', '2025-06-20 12:10:00', 117500, 9.973504, 1, 2),('TRUMP', '2025-06-20 12:10:15', 118500, 0.267536, 1, 2),('TRUMP', '2025-06-20 12:10:30', 117300, 4.082676, 1, 2),('TRUMP', '2025-06-20 12:10:45', 118000, 9.034284, 1, 2),('TRUMP', '2025-06-20 12:11:00', 118500, 4.905567, 1, 2),('TRUMP', '2025-06-20 12:11:15', 119500, 1.809977, 1, 2),('TRUMP', '2025-06-20 12:11:30', 118300, 0.161625, 1, 2),('TRUMP', '2025-06-20 12:11:45', 119000, 1.387359, 1, 2),('TRUMP', '2025-06-20 12:12:00', 119500, 1.854479, 1, 2),('TRUMP', '2025-06-20 12:12:15', 120500, 3.767219, 1, 2),('TRUMP', '2025-06-20 12:12:30', 119300, 1.859272, 1, 2),('TRUMP', '2025-06-20 12:12:45', 120000, 8.522854, 1, 2),('TRUMP', '2025-06-20 12:13:00', 120500, 3.338731, 1, 2),('TRUMP', '2025-06-20 12:13:15', 121500, 5.086795, 1, 2),('TRUMP', '2025-06-20 12:13:30', 120300, 7.859067, 1, 2),('TRUMP', '2025-06-20 12:13:45', 121000, 0.478668, 1, 2),('TRUMP', '2025-06-20 12:14:00', 121500, 5.280367, 1, 2),('TRUMP', '2025-06-20 12:14:15', 122500, 9.380323, 1, 2),('TRUMP', '2025-06-20 12:14:30', 121300, 7.313775, 1, 2),('TRUMP', '2025-06-20 12:14:45', 122000, 6.960436, 1, 2),('TRUMP', '2025-06-20 12:15:00', 122500, 9.869723, 1, 2),('TRUMP', '2025-06-20 12:15:15', 123500, 4.719234, 1, 2),('TRUMP', '2025-06-20 12:15:30', 122300, 2.645146, 1, 2),('TRUMP', '2025-06-20 12:15:45', 123000, 2.935895, 1, 2),('TRUMP', '2025-06-20 12:16:00', 123500, 1.968424, 1, 2),('TRUMP', '2025-06-20 12:16:15', 124500, 3.023982, 1, 2),('TRUMP', '2025-06-20 12:16:30', 123300, 6.625944, 1, 2),('TRUMP', '2025-06-20 12:16:45', 124000, 0.570876, 1, 2),('TRUMP', '2025-06-20 12:17:00', 124500, 0.031117, 1, 2),('TRUMP', '2025-06-20 12:17:15', 125500, 0.259447, 1, 2),('TRUMP', '2025-06-20 12:17:30', 124300, 6.973692, 1, 2),('TRUMP', '2025-06-20 12:17:45', 125000, 4.259961, 1, 2),('TRUMP', '2025-06-20 12:18:00', 125500, 5.349842, 1, 2),('TRUMP', '2025-06-20 12:18:15', 126500, 6.90704, 1, 2),('TRUMP', '2025-06-20 12:18:30', 125300, 7.151793, 1, 2),('TRUMP', '2025-06-20 12:18:45', 126000, 1.511567, 1, 2),('TRUMP', '2025-06-20 12:19:00', 126500, 8.571635, 1, 2),('TRUMP', '2025-06-20 12:19:15', 127500, 7.144139, 1, 2),('TRUMP', '2025-06-20 12:19:30', 126300, 1.737094, 1, 2),('TRUMP', '2025-06-20 12:19:45', 127000, 7.701161, 1, 2),('TRUMP', '2025-06-20 12:20:00', 127500, 1.085378, 1, 2),('TRUMP', '2025-06-20 12:20:15', 128500, 6.917887, 1, 2),('TRUMP', '2025-06-20 12:20:30', 127300, 0.425277, 1, 2),('TRUMP', '2025-06-20 12:20:45', 128000, 6.632266, 1, 2),('TRUMP', '2025-06-20 12:21:00', 128500, 9.441446, 1, 2),('TRUMP', '2025-06-20 12:21:15', 129500, 3.955151, 1, 2),('TRUMP', '2025-06-20 12:21:30', 128300, 7.993368, 1, 2),('TRUMP', '2025-06-20 12:21:45', 129000, 5.353055, 1, 2),('TRUMP', '2025-06-20 12:22:00', 129500, 5.969741, 1, 2),('TRUMP', '2025-06-20 12:22:15', 130500, 8.009607, 1, 2),('TRUMP', '2025-06-20 12:22:30', 129300, 4.028762, 1, 2),('TRUMP', '2025-06-20 12:22:45', 130000, 9.228241, 1, 2),('TRUMP', '2025-06-20 12:23:00', 130500, 6.860371, 1, 2),('TRUMP', '2025-06-20 12:23:15', 131500, 9.642695, 1, 2),('TRUMP', '2025-06-20 12:23:30', 130300, 2.854076, 1, 2),('TRUMP', '2025-06-20 12:23:45', 131000, 2.116211, 1, 2),('TRUMP', '2025-06-20 12:24:00', 131500, 1.971906, 1, 2),('TRUMP', '2025-06-20 12:24:15', 132500, 9.627731, 1, 2),('TRUMP', '2025-06-20 12:24:30', 131300, 3.451863, 1, 2),('TRUMP', '2025-06-20 12:24:45', 132000, 8.480449, 1, 2),('TRUMP', '2025-06-20 12:25:00', 132500, 2.968231, 1, 2),('TRUMP', '2025-06-20 12:25:15', 133500, 9.518408, 1, 2),('TRUMP', '2025-06-20 12:25:30', 132300, 9.614225, 1, 2),('TRUMP', '2025-06-20 12:25:45', 133000, 4.649204, 1, 2),('TRUMP', '2025-06-20 12:26:00', 133500, 5.599086, 1, 2),('TRUMP', '2025-06-20 12:26:15', 134500, 2.309748, 1, 2),('TRUMP', '2025-06-20 12:26:30', 133300, 4.826003, 1, 2),('TRUMP', '2025-06-20 12:26:45', 134000, 7.461383, 1, 2),('TRUMP', '2025-06-20 12:27:00', 134500, 5.499619, 1, 2),('TRUMP', '2025-06-20 12:27:15', 135500, 1.870368, 1, 2),('TRUMP', '2025-06-20 12:27:30', 134300, 1.770907, 1, 2),('TRUMP', '2025-06-20 12:27:45', 135000, 7.403058, 1, 2),('TRUMP', '2025-06-20 12:28:00', 135500, 5.180506, 1, 2),('TRUMP', '2025-06-20 12:28:15', 136500, 4.978612, 1, 2),('TRUMP', '2025-06-20 12:28:30', 135300, 7.764368, 1, 2),('TRUMP', '2025-06-20 12:28:45', 136000, 4.485013, 1, 2),('TRUMP', '2025-06-20 12:29:00', 136500, 7.679501, 1, 2),('TRUMP', '2025-06-20 12:29:15', 137500, 0.251638, 1, 2),('TRUMP', '2025-06-20 12:29:30', 136300, 5.19735, 1, 2),('TRUMP', '2025-06-20 12:29:45', 137000, 6.083785, 1, 2),('TRUMP', '2025-06-20 12:30:00', 137500, 7.841009, 1, 2),('TRUMP', '2025-06-20 12:30:15', 138500, 1.305749, 1, 2),('TRUMP', '2025-06-20 12:30:30', 137300, 9.923748, 1, 2),('TRUMP', '2025-06-20 12:30:45', 138000, 5.389908, 1, 2),('TRUMP', '2025-06-20 12:31:00', 138500, 0.330078, 1, 2),('TRUMP', '2025-06-20 12:31:15', 139500, 6.947909, 1, 2),('TRUMP', '2025-06-20 12:31:30', 138300, 0.650909, 1, 2),('TRUMP', '2025-06-20 12:31:45', 139000, 6.047228, 1, 2),('TRUMP', '2025-06-20 12:32:00', 139500, 9.255117, 1, 2),('TRUMP', '2025-06-20 12:32:15', 140500, 5.451498, 1, 2),('TRUMP', '2025-06-20 12:32:30', 139300, 1.31332, 1, 2),('TRUMP', '2025-06-20 12:32:45', 140000, 3.022989, 1, 2),('TRUMP', '2025-06-20 12:33:00', 140500, 4.092964, 1, 2),('TRUMP', '2025-06-20 12:33:15', 141500, 6.853602, 1, 2),('TRUMP', '2025-06-20 12:33:30', 140300, 1.077591, 1, 2),('TRUMP', '2025-06-20 12:33:45', 141000, 8.605654, 1, 2),('TRUMP', '2025-06-20 12:34:00', 141500, 3.053348, 1, 2),('TRUMP', '2025-06-20 12:34:15', 142500, 9.238291, 1, 2),('TRUMP', '2025-06-20 12:34:30', 141300, 6.557596, 1, 2),('TRUMP', '2025-06-20 12:34:45', 142000, 0.930659, 1, 2),('TRUMP', '2025-06-20 12:35:00', 142500, 6.844459, 1, 2),('TRUMP', '2025-06-20 12:35:15', 143500, 7.34284, 1, 2),('TRUMP', '2025-06-20 12:35:30', 142300, 3.903414, 1, 2),('TRUMP', '2025-06-20 12:35:45', 143000, 6.101143, 1, 2),('TRUMP', '2025-06-20 12:36:00', 143500, 7.558135, 1, 2),('TRUMP', '2025-06-20 12:36:15', 144500, 2.691521, 1, 2),('TRUMP', '2025-06-20 12:36:30', 143300, 0.498199, 1, 2),('TRUMP', '2025-06-20 12:36:45', 144000, 6.531172, 1, 2),('TRUMP', '2025-06-20 12:37:00', 144500, 8.207251, 1, 2),('TRUMP', '2025-06-20 12:37:15', 145500, 2.177845, 1, 2),('TRUMP', '2025-06-20 12:37:30', 144300, 0.819495, 1, 2),('TRUMP', '2025-06-20 12:37:45', 145000, 1.931549, 1, 2),('TRUMP', '2025-06-20 12:38:00', 145500, 3.160184, 1, 2),('TRUMP', '2025-06-20 12:38:15', 146500, 2.943221, 1, 2),('TRUMP', '2025-06-20 12:38:30', 145300, 4.462713, 1, 2),('TRUMP', '2025-06-20 12:38:45', 146000, 3.048597, 1, 2),('TRUMP', '2025-06-20 12:39:00', 146500, 7.924749, 1, 2),('TRUMP', '2025-06-20 12:39:15', 147500, 5.157541, 1, 2),('TRUMP', '2025-06-20 12:39:30', 146300, 5.243456, 1, 2),('TRUMP', '2025-06-20 12:39:45', 147000, 5.511854, 1, 2),('TRUMP', '2025-06-20 12:40:00', 147500, 8.327237, 1, 2),('TRUMP', '2025-06-20 12:40:15', 148500, 0.859506, 1, 2),('TRUMP', '2025-06-20 12:40:30', 147300, 8.875903, 1, 2),('TRUMP', '2025-06-20 12:40:45', 148000, 6.879751, 1, 2),('TRUMP', '2025-06-20 12:41:00', 148500, 5.766268, 1, 2),('TRUMP', '2025-06-20 12:41:15', 149500, 5.865001, 1, 2),('TRUMP', '2025-06-20 12:41:30', 148300, 7.898954, 1, 2),('TRUMP', '2025-06-20 12:41:45', 149000, 9.90274, 1, 2),('TRUMP', '2025-06-20 12:42:00', 149500, 0.10262, 1, 2),('TRUMP', '2025-06-20 12:42:15', 150500, 1.283948, 1, 2),('TRUMP', '2025-06-20 12:42:30', 149300, 5.752982, 1, 2),('TRUMP', '2025-06-20 12:42:45', 150000, 0.496927, 1, 2),('TRUMP', '2025-06-20 12:43:00', 150500, 3.366316, 1, 2),('TRUMP', '2025-06-20 12:43:15', 151500, 1.156721, 1, 2),('TRUMP', '2025-06-20 12:43:30', 150300, 9.979606, 1, 2),('TRUMP', '2025-06-20 12:43:45', 151000, 0.270457, 1, 2),('TRUMP', '2025-06-20 12:44:00', 151500, 8.882495, 1, 2),('TRUMP', '2025-06-20 12:44:15', 152500, 9.897505, 1, 2),('TRUMP', '2025-06-20 12:44:30', 151300, 8.765969, 1, 2),('TRUMP', '2025-06-20 12:44:45', 152000, 7.602747, 1, 2),('TRUMP', '2025-06-20 12:45:00', 152500, 0.065239, 1, 2),('TRUMP', '2025-06-20 12:45:15', 153500, 1.063647, 1, 2),('TRUMP', '2025-06-20 12:45:30', 152300, 6.123133, 1, 2),('TRUMP', '2025-06-20 12:45:45', 153000, 8.914352, 1, 2),('TRUMP', '2025-06-20 12:46:00', 153500, 9.833871, 1, 2),('TRUMP', '2025-06-20 12:46:15', 154500, 7.707631, 1, 2),('TRUMP', '2025-06-20 12:46:30', 153300, 8.691414, 1, 2),('TRUMP', '2025-06-20 12:46:45', 154000, 2.251465, 1, 2),('TRUMP', '2025-06-20 12:47:00', 154500, 1.594882, 1, 2),('TRUMP', '2025-06-20 12:47:15', 155500, 4.099762, 1, 2),('TRUMP', '2025-06-20 12:47:30', 154300, 8.966404, 1, 2),('TRUMP', '2025-06-20 12:47:45', 155000, 9.960587, 1, 2),('TRUMP', '2025-06-20 12:48:00', 155500, 7.109742, 1, 2),('TRUMP', '2025-06-20 12:48:15', 156500, 9.441154, 1, 2),('TRUMP', '2025-06-20 12:48:30', 155300, 1.647429, 1, 2),('TRUMP', '2025-06-20 12:48:45', 156000, 1.932711, 1, 2),('TRUMP', '2025-06-20 12:49:00', 156500, 1.94886, 1, 2),('TRUMP', '2025-06-20 12:49:15', 157500, 3.382305, 1, 2),('TRUMP', '2025-06-20 12:49:30', 156300, 8.419782, 1, 2),('TRUMP', '2025-06-20 12:49:45', 157000, 7.421292, 1, 2),('TRUMP', '2025-06-20 12:50:00', 157500, 3.463679, 1, 2),('TRUMP', '2025-06-20 12:50:15', 158500, 3.352233, 1, 2),('TRUMP', '2025-06-20 12:50:30', 157300, 6.833118, 1, 2),('TRUMP', '2025-06-20 12:50:45', 158000, 2.638927, 1, 2),('TRUMP', '2025-06-20 12:51:00', 158500, 8.334976, 1, 2),('TRUMP', '2025-06-20 12:51:15', 159500, 7.926957, 1, 2),('TRUMP', '2025-06-20 12:51:30', 158300, 6.060456, 1, 2),('TRUMP', '2025-06-20 12:51:45', 159000, 6.434992, 1, 2),('TRUMP', '2025-06-20 12:52:00', 159500, 6.991396, 1, 2),('TRUMP', '2025-06-20 12:52:15', 160500, 0.873365, 1, 2),('TRUMP', '2025-06-20 12:52:30', 159300, 8.377699, 1, 2),('TRUMP', '2025-06-20 12:52:45', 160000, 0.807103, 1, 2),('TRUMP', '2025-06-20 12:53:00', 160500, 5.667394, 1, 2),('TRUMP', '2025-06-20 12:53:15', 161500, 6.738744, 1, 2),('TRUMP', '2025-06-20 12:53:30', 160300, 8.858057, 1, 2),('TRUMP', '2025-06-20 12:53:45', 161000, 5.638788, 1, 2),('TRUMP', '2025-06-20 12:54:00', 161500, 3.388675, 1, 2),('TRUMP', '2025-06-20 12:54:15', 162500, 7.381115, 1, 2),('TRUMP', '2025-06-20 12:54:30', 161300, 9.568616, 1, 2),('TRUMP', '2025-06-20 12:54:45', 162000, 1.416617, 1, 2),('TRUMP', '2025-06-20 12:55:00', 162500, 5.899368, 1, 2),('TRUMP', '2025-06-20 12:55:15', 163500, 1.497177, 1, 2),('TRUMP', '2025-06-20 12:55:30', 162300, 9.656289, 1, 2),('TRUMP', '2025-06-20 12:55:45', 163000, 5.267589, 1, 2),('TRUMP', '2025-06-20 12:56:00', 163500, 7.263686, 1, 2),('TRUMP', '2025-06-20 12:56:15', 164500, 6.813319, 1, 2),('TRUMP', '2025-06-20 12:56:30', 163300, 6.61918, 1, 2),('TRUMP', '2025-06-20 12:56:45', 164000, 8.719159, 1, 2),('TRUMP', '2025-06-20 12:57:00', 164500, 6.104387, 1, 2),('TRUMP', '2025-06-20 12:57:15', 165500, 7.364349, 1, 2),('TRUMP', '2025-06-20 12:57:30', 164300, 2.575814, 1, 2),('TRUMP', '2025-06-20 12:57:45', 165000, 2.500916, 1, 2),('TRUMP', '2025-06-20 12:58:00', 165500, 4.890084, 1, 2),('TRUMP', '2025-06-20 12:58:15', 166500, 2.602317, 1, 2),('TRUMP', '2025-06-20 12:58:30', 165300, 4.841235, 1, 2),('TRUMP', '2025-06-20 12:58:45', 166000, 2.15698, 1, 2),('TRUMP', '2025-06-20 12:59:00', 166500, 4.561982, 1, 2),('TRUMP', '2025-06-20 12:59:15', 167500, 6.042331, 1, 2),('TRUMP', '2025-06-20 12:59:30', 166300, 5.52763, 1, 2),('TRUMP', '2025-06-20 12:59:45', 167000, 8.559315, 1, 2),('TRUMP', '2025-06-20 13:00:00', 167500, 3.460241, 1, 2),('TRUMP', '2025-06-20 13:00:15', 168500, 7.567345, 1, 2),('TRUMP', '2025-06-20 13:00:30', 167300, 7.61122, 1, 2),('TRUMP', '2025-06-20 13:00:45', 168000, 5.421229, 1, 2),('TRUMP', '2025-06-20 13:01:00', 168500, 4.206138, 1, 2),('TRUMP', '2025-06-20 13:01:15', 169500, 4.208376, 1, 2),('TRUMP', '2025-06-20 13:01:30', 168300, 0.212471, 1, 2),('TRUMP', '2025-06-20 13:01:45', 169000, 9.257726, 1, 2),('TRUMP', '2025-06-20 13:02:00', 169500, 6.435321, 1, 2),('TRUMP', '2025-06-20 13:02:15', 170500, 3.045099, 1, 2),('TRUMP', '2025-06-20 13:02:30', 169300, 6.324599, 1, 2),('TRUMP', '2025-06-20 13:02:45', 170000, 6.656156, 1, 2),('TRUMP', '2025-06-20 13:03:00', 170500, 7.307695, 1, 2),('TRUMP', '2025-06-20 13:03:15', 171500, 8.97098, 1, 2),('TRUMP', '2025-06-20 13:03:30', 170300, 2.229385, 1, 2),('TRUMP', '2025-06-20 13:03:45', 171000, 3.917124, 1, 2),('TRUMP', '2025-06-20 13:04:00', 171500, 7.539298, 1, 2),('TRUMP', '2025-06-20 13:04:15', 172500, 5.462192, 1, 2),('TRUMP', '2025-06-20 13:04:30', 171300, 7.630677, 1, 2),('TRUMP', '2025-06-20 13:04:45', 172000, 8.12458, 1, 2),('TRUMP', '2025-06-20 13:05:00', 172500, 1.517659, 1, 2),('TRUMP', '2025-06-20 13:05:15', 173500, 0.512998, 1, 2),('TRUMP', '2025-06-20 13:05:30', 172300, 1.57843, 1, 2),('TRUMP', '2025-06-20 13:05:45', 173000, 7.496923, 1, 2),('TRUMP', '2025-06-20 13:06:00', 173500, 8.282174, 1, 2),('TRUMP', '2025-06-20 13:06:15', 174500, 2.860609, 1, 2),('TRUMP', '2025-06-20 13:06:30', 173300, 0.988743, 1, 2),('TRUMP', '2025-06-20 13:06:45', 174000, 8.553508, 1, 2),('TRUMP', '2025-06-20 13:07:00', 174500, 4.461439, 1, 2),('TRUMP', '2025-06-20 13:07:15', 175500, 0.774712, 1, 2),('TRUMP', '2025-06-20 13:07:30', 174300, 2.975839, 1, 2),('TRUMP', '2025-06-20 13:07:45', 175000, 2.207572, 1, 2),('TRUMP', '2025-06-20 13:08:00', 175500, 1.383143, 1, 2),('TRUMP', '2025-06-20 13:08:15', 176500, 4.008495, 1, 2),('TRUMP', '2025-06-20 13:08:30', 175300, 6.344609, 1, 2),('TRUMP', '2025-06-20 13:08:45', 176000, 9.382147, 1, 2),('TRUMP', '2025-06-20 13:09:00', 176500, 1.424894, 1, 2),('TRUMP', '2025-06-20 13:09:15', 177500, 7.540564, 1, 2),('TRUMP', '2025-06-20 13:09:30', 176300, 6.460262, 1, 2),('TRUMP', '2025-06-20 13:09:45', 177000, 1.071227, 1, 2),('TRUMP', '2025-06-20 13:10:00', 177500, 3.302173, 1, 2),('TRUMP', '2025-06-20 13:10:15', 178500, 1.435476, 1, 2),('TRUMP', '2025-06-20 13:10:30', 177300, 0.539374, 1, 2),('TRUMP', '2025-06-20 13:10:45', 178000, 2.874828, 1, 2),('TRUMP', '2025-06-20 13:11:00', 178500, 4.324765, 1, 2),('TRUMP', '2025-06-20 13:11:15', 179500, 4.986065, 1, 2),('TRUMP', '2025-06-20 13:11:30', 178300, 6.788839, 1, 2),('TRUMP', '2025-06-20 13:11:45', 179000, 5.838954, 1, 2),('TRUMP', '2025-06-20 13:12:00', 179500, 5.670572, 1, 2),('TRUMP', '2025-06-20 13:12:15', 180500, 0.100339, 1, 2),('TRUMP', '2025-06-20 13:12:30', 179300, 0.610405, 1, 2),('TRUMP', '2025-06-20 13:12:45', 180000, 5.831767, 1, 2),('TRUMP', '2025-06-20 13:13:00', 180500, 6.203251, 1, 2),('TRUMP', '2025-06-20 13:13:15', 181500, 0.002601, 1, 2),('TRUMP', '2025-06-20 13:13:30', 180300, 8.444234, 1, 2),('TRUMP', '2025-06-20 13:13:45', 181000, 0.71703, 1, 2),('TRUMP', '2025-06-20 13:14:00', 181500, 8.543699, 1, 2),('TRUMP', '2025-06-20 13:14:15', 182500, 8.7045, 1, 2),('TRUMP', '2025-06-20 13:14:30', 181300, 8.026515, 1, 2),('TRUMP', '2025-06-20 13:14:45', 182000, 9.150413, 1, 2),('TRUMP', '2025-06-20 13:15:00', 182500, 0.864229, 1, 2),('TRUMP', '2025-06-20 13:15:15', 183500, 1.286307, 1, 2),('TRUMP', '2025-06-20 13:15:30', 182300, 0.436882, 1, 2),('TRUMP', '2025-06-20 13:15:45', 183000, 7.25809, 1, 2),('TRUMP', '2025-06-20 13:16:00', 183500, 2.856008, 1, 2),('TRUMP', '2025-06-20 13:16:15', 184500, 9.239982, 1, 2),('TRUMP', '2025-06-20 13:16:30', 183300, 9.566522, 1, 2),('TRUMP', '2025-06-20 13:16:45', 184000, 6.027277, 1, 2),('TRUMP', '2025-06-20 13:17:00', 184500, 9.503485, 1, 2),('TRUMP', '2025-06-20 13:17:15', 185500, 9.159011, 1, 2),('TRUMP', '2025-06-20 13:17:30', 184300, 8.26995, 1, 2),('TRUMP', '2025-06-20 13:17:45', 185000, 4.063992, 1, 2),('TRUMP', '2025-06-20 13:18:00', 185500, 0.274964, 1, 2),('TRUMP', '2025-06-20 13:18:15', 186500, 4.024919, 1, 2),('TRUMP', '2025-06-20 13:18:30', 185300, 8.694652, 1, 2),('TRUMP', '2025-06-20 13:18:45', 186000, 2.332739, 1, 2),('TRUMP', '2025-06-20 13:19:00', 186500, 2.600088, 1, 2),('TRUMP', '2025-06-20 13:19:15', 187500, 5.066279, 1, 2),('TRUMP', '2025-06-20 13:19:30', 186300, 1.005504, 1, 2),('TRUMP', '2025-06-20 13:19:45', 187000, 3.784397, 1, 2),('TRUMP', '2025-06-20 13:20:00', 187500, 7.360888, 1, 2),('TRUMP', '2025-06-20 13:20:15', 188500, 3.009192, 1, 2),('TRUMP', '2025-06-20 13:20:30', 187300, 2.426659, 1, 2),('TRUMP', '2025-06-20 13:20:45', 188000, 1.398995, 1, 2),('TRUMP', '2025-06-20 13:21:00', 188500, 3.007769, 1, 2),('TRUMP', '2025-06-20 13:21:15', 189500, 6.170195, 1, 2),('TRUMP', '2025-06-20 13:21:30', 188300, 5.617552, 1, 2),('TRUMP', '2025-06-20 13:21:45', 189000, 5.213175, 1, 2),('TRUMP', '2025-06-20 13:22:00', 189500, 4.196965, 1, 2),('TRUMP', '2025-06-20 13:22:15', 190500, 4.825218, 1, 2),('TRUMP', '2025-06-20 13:22:30', 189300, 8.473475, 1, 2),('TRUMP', '2025-06-20 13:22:45', 190000, 4.263057, 1, 2),('TRUMP', '2025-06-20 13:23:00', 190500, 5.578619, 1, 2),('TRUMP', '2025-06-20 13:23:15', 191500, 4.544397, 1, 2),('TRUMP', '2025-06-20 13:23:30', 190300, 1.713347, 1, 2),('TRUMP', '2025-06-20 13:23:45', 191000, 1.599596, 1, 2),('TRUMP', '2025-06-20 13:24:00', 191500, 3.815273, 1, 2),('TRUMP', '2025-06-20 13:24:15', 192500, 0.92584, 1, 2),('TRUMP', '2025-06-20 13:24:30', 191300, 1.581303, 1, 2),('TRUMP', '2025-06-20 13:24:45', 192000, 6.01344, 1, 2),('TRUMP', '2025-06-20 13:25:00', 192500, 7.906073, 1, 2),('TRUMP', '2025-06-20 13:25:15', 193500, 5.342963, 1, 2),('TRUMP', '2025-06-20 13:25:30', 192300, 4.425103, 1, 2),('TRUMP', '2025-06-20 13:25:45', 193000, 8.136868, 1, 2),('TRUMP', '2025-06-20 13:26:00', 193500, 4.658685, 1, 2),('TRUMP', '2025-06-20 13:26:15', 194500, 2.968818, 1, 2),('TRUMP', '2025-06-20 13:26:30', 193300, 0.350662, 1, 2),('TRUMP', '2025-06-20 13:26:45', 194000, 5.12039, 1, 2),('TRUMP', '2025-06-20 13:27:00', 194500, 1.1645, 1, 2),('TRUMP', '2025-06-20 13:27:15', 195500, 0.771764, 1, 2),('TRUMP', '2025-06-20 13:27:30', 194300, 8.484028, 1, 2),('TRUMP', '2025-06-20 13:27:45', 195000, 0.073982, 1, 2),('TRUMP', '2025-06-20 13:28:00', 195500, 7.349262, 1, 2),('TRUMP', '2025-06-20 13:28:15', 196500, 2.393077, 1, 2),('TRUMP', '2025-06-20 13:28:30', 195300, 7.744781, 1, 2),('TRUMP', '2025-06-20 13:28:45', 196000, 4.049918, 1, 2),('TRUMP', '2025-06-20 13:29:00', 196500, 4.970718, 1, 2),('TRUMP', '2025-06-20 13:29:15', 197500, 0.555312, 1, 2),('TRUMP', '2025-06-20 13:29:30', 196300, 9.801287, 1, 2),('TRUMP', '2025-06-20 13:29:45', 197000, 7.769644, 1, 2),('TRUMP', '2025-06-20 13:30:00', 197500, 7.402004, 1, 2),('TRUMP', '2025-06-20 13:30:15', 198500, 7.52452, 1, 2),('TRUMP', '2025-06-20 13:30:30', 197300, 8.828752, 1, 2),('TRUMP', '2025-06-20 13:30:45', 198000, 8.245594, 1, 2),('TRUMP', '2025-06-20 13:31:00', 198500, 3.805639, 1, 2),('TRUMP', '2025-06-20 13:31:15', 199500, 0.388988, 1, 2),('TRUMP', '2025-06-20 13:31:30', 198300, 6.215976, 1, 2),('TRUMP', '2025-06-20 13:31:45', 199000, 5.831205, 1, 2),('TRUMP', '2025-06-20 13:32:00', 199500, 0.961642, 1, 2),('TRUMP', '2025-06-20 13:32:15', 200500, 7.759683, 1, 2),('TRUMP', '2025-06-20 13:32:30', 199300, 6.65478, 1, 2),('TRUMP', '2025-06-20 13:32:45', 200000, 4.554254, 1, 2),('TRUMP', '2025-06-20 13:33:00', 200500, 0.332403, 1, 2),('TRUMP', '2025-06-20 13:33:15', 201500, 3.366485, 1, 2),('TRUMP', '2025-06-20 13:33:30', 200300, 8.546545, 1, 2),('TRUMP', '2025-06-20 13:33:45', 201000, 2.103162, 1, 2),('TRUMP', '2025-06-20 13:34:00', 201500, 4.490324, 1, 2),('TRUMP', '2025-06-20 13:34:15', 202500, 5.217917, 1, 2),('TRUMP', '2025-06-20 13:34:30', 201300, 6.816458, 1, 2),('TRUMP', '2025-06-20 13:34:45', 202000, 3.149732, 1, 2),('TRUMP', '2025-06-20 13:35:00', 202500, 7.410233, 1, 2),('TRUMP', '2025-06-20 13:35:15', 203500, 2.357406, 1, 2),('TRUMP', '2025-06-20 13:35:30', 202300, 8.662748, 1, 2),('TRUMP', '2025-06-20 13:35:45', 203000, 6.765646, 1, 2),('TRUMP', '2025-06-20 13:36:00', 203500, 2.364812, 1, 2),('TRUMP', '2025-06-20 13:36:15', 204500, 2.591432, 1, 2),('TRUMP', '2025-06-20 13:36:30', 203300, 7.761005, 1, 2),('TRUMP', '2025-06-20 13:36:45', 204000, 0.610109, 1, 2),('TRUMP', '2025-06-20 13:37:00', 204500, 5.989301, 1, 2),('TRUMP', '2025-06-20 13:37:15', 205500, 1.649262, 1, 2),('TRUMP', '2025-06-20 13:37:30', 204300, 2.433316, 1, 2),('TRUMP', '2025-06-20 13:37:45', 205000, 1.873075, 1, 2),('TRUMP', '2025-06-20 13:38:00', 205500, 3.521584, 1, 2),('TRUMP', '2025-06-20 13:38:15', 206500, 2.420859, 1, 2),('TRUMP', '2025-06-20 13:38:30', 205300, 1.076099, 1, 2),('TRUMP', '2025-06-20 13:38:45', 206000, 8.17741, 1, 2),('TRUMP', '2025-06-20 13:39:00', 206500, 7.865186, 1, 2),('TRUMP', '2025-06-20 13:39:15', 207500, 1.881793, 1, 2),('TRUMP', '2025-06-20 13:39:30', 206300, 0.161313, 1, 2),('TRUMP', '2025-06-20 13:39:45', 207000, 8.332123, 1, 2),('TRUMP', '2025-06-20 13:40:00', 207500, 8.125667, 1, 2),('TRUMP', '2025-06-20 13:40:15', 208500, 2.293223, 1, 2),('TRUMP', '2025-06-20 13:40:30', 207300, 1.796608, 1, 2),('TRUMP', '2025-06-20 13:40:45', 208000, 2.256854, 1, 2),('TRUMP', '2025-06-20 13:41:00', 208500, 9.2085, 1, 2),('TRUMP', '2025-06-20 13:41:15', 209500, 9.813774, 1, 2),('TRUMP', '2025-06-20 13:41:30', 208300, 7.63887, 1, 2),('TRUMP', '2025-06-20 13:41:45', 209000, 7.339151, 1, 2); \ No newline at end of file