diff --git a/build.gradle b/build.gradle index ecfc5b0a..1e947b67 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' implementation 'org.springframework.boot:spring-boot-starter-actuator' - + implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.13.1' 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 8fa1e380..345806a7 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -4,12 +4,16 @@ 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; @@ -29,16 +33,20 @@ public class ApiScheduler { private final Map lastSequentialIdMap = new ConcurrentHashMap<>(); private final AssetRepository assetRepository; private final CoinoneAPIClient coinoneAPIClient; + private final VWAPMetricsRecorder recorder; + private final MeterRegistry meterRegistry; private String ticker; -// @Scheduled(fixedRate = 5000) + @Scheduled(fixedRate = 5000) public void MarketAllRequest() throws InterruptedException { - List tickers = assetRepository.findAll(); - for (Asset ticker : tickers){ - String tickerName = ticker.getTicker(); - MarketDataRequest(tickerName); -// Thread.sleep(500); - } + Timer timer = meterRegistry.timer("apischeduler.request.duration"); + timer.record(() -> { + List tickers = assetRepository.findAll(); + for (Asset ticker : tickers) { + String tickerName = ticker.getTicker(); + MarketDataRequest(tickerName); + } + }); } public void MarketDataRequest(String ticker){ @@ -62,7 +70,10 @@ public void MarketDataRequest(String ticker){ lastSequentialIdMap.put(ticker,lastSeqId); double vwap = apiVWAPState.getVWAP(); double volume = apiVWAPState.getAvgVolumePerOrder(); + recorder.recordApiVwap(ticker,vwap); + orderGenerateService.generateOrder(ticker,vwap,volume); //1tick 당 매수/매도 3개씩 제작 + // log.info("작동확인 {}의 가격 : {} , 볼륨 : {}",ticker, vwap, volume); } @@ -89,7 +100,7 @@ public void destroy() throws Exception { //담긴 Queue데이터 확인용 } catch (Exception e) { log.error("Bithumb API 오류 발생: {} → Coinone으로 대체 요청", e.getMessage()); - return coinoneAPIClient.get(ticker); + return coinoneAPIClient.geta(ticker); } }*/ diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java index db3f2c8f..eafe3a3a 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java @@ -10,14 +10,14 @@ import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j -@Service +@Component @RequiredArgsConstructor public class UnitPriceRefresher implements ApplicationRunner { private final UnitPricePolicy unitPricePolicy; diff --git a/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java b/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java new file mode 100644 index 00000000..7eaf4177 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java @@ -0,0 +1,18 @@ +package com.cleanengine.coin.realitybot.config; + +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +//@Profile("actuator") +@Configuration +public class MetricConfig { + + @Bean + MeterRegistryCustomizer metricsCommonTags() { + return registry -> registry.config().commonTags("application", "my-app"); + } + +} 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 fa22e48b..aa801573 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java @@ -17,8 +17,10 @@ public void addTick(Ticks tick){ if (ticksQueue.size() >= maxQueueSize) { //10개 이상이 되면 선착순으로 제거해나감 Ticks removed = ticksQueue.poll(); + if (removed != null){ calculator.removeTrade(removed.getTrade_price(), removed.getTrade_volume()); - } + } + } //초기엔 들어온 갯수에 따라 증가시켜서 계산함 ticksQueue.add(tick); calculator.recordTrade(tick.getTrade_price(),tick.getTrade_volume()); diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java new file mode 100644 index 00000000..1d960704 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java @@ -0,0 +1,65 @@ +package com.cleanengine.coin.realitybot.domain; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +@Component +@RequiredArgsConstructor +public class VWAPMetricsRecorder { + private final MeterRegistry meterRegistry; + private final ConcurrentHashMap> orderPriceMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap orderPriceSummery = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> apiVwapMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> platformVwapMap = new ConcurrentHashMap<>(); + + public void recordPrice(String ticker, boolean isBuy, double price){ + String type = isBuy ? "buy" : "sell"; + String timeStamp = Instant.now().toString(); +// String key = ticker +"|"+type+"|"+timeStamp; + String key = ticker +"|"+type; + + + AtomicReference priceRef = orderPriceMap.computeIfAbsent(key, k -> { + + AtomicReference value = new AtomicReference<>(price); + + meterRegistry.gauge("order_price", + Tags.of("ticker",ticker,"type",type) + ,value,AtomicReference::get); + return value; + }); + priceRef.set(price); + + DistributionSummary summary = orderPriceSummery.computeIfAbsent(key,k -> + DistributionSummary.builder("order_price_summary") + .tags(Tags.of("ticker",ticker,"type",type)) + .publishPercentiles(0.05,0.95) + .register(meterRegistry) + ); + summary.record(price); + } + + + public void recordApiVwap(String ticker, double price){ + apiVwapMap.computeIfAbsent(ticker, t -> { + AtomicReference ref = new AtomicReference<>(price); + meterRegistry.gauge("api_vwap",Tags.of("ticker",t),ref,AtomicReference::get); + return ref; + }).set(price); + } + public void recordPlatformVwap(String ticker, double price){ + platformVwapMap.computeIfAbsent(ticker, t -> { + AtomicReference ref = new AtomicReference<>(price); + meterRegistry.gauge("platform_vwap",Tags.of("ticker",t),ref,AtomicReference::get); + return ref; + }).set(price); + } + +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java b/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java index 9513aab6..438246e6 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java +++ b/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java @@ -12,7 +12,7 @@ public class Ticks { private String trade_date_utc; // LocalDate private String trade_time_utc; // LocalTime private String timestamp; //instant 에러 발생 - private float trade_price; + private double trade_price; private double trade_volume; private float prev_closing_price; private double change_price; 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 4d2c878a..17c0599d 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -1,14 +1,13 @@ package com.cleanengine.coin.realitybot.service; +import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository; +import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository; import com.cleanengine.coin.order.application.OrderService; import com.cleanengine.coin.realitybot.api.UnitPriceRefresher; +import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder; import com.cleanengine.coin.realitybot.vo.DeviationPricePolicy; import com.cleanengine.coin.realitybot.vo.OrderPricePolicy; import com.cleanengine.coin.realitybot.vo.OrderVolumePolicy; -import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository; -import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository; -import com.cleanengine.coin.trade.entity.Trade; -import com.cleanengine.coin.trade.repository.TradeRepository; import com.cleanengine.coin.user.domain.Account; import com.cleanengine.coin.user.domain.Wallet; import lombok.RequiredArgsConstructor; @@ -18,8 +17,6 @@ import org.springframework.stereotype.Service; import java.text.DecimalFormat; -import java.util.List; -import java.util.concurrent.TimeUnit; import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; @@ -29,19 +26,20 @@ @Order(5) @RequiredArgsConstructor public class OrderGenerateService { + private final VWAPMetricsRecorder VWAPMetricsRecorder; @Value("${bot-handler.order-level}") private int[] orderLevels; //체결 강도 private double unitPrice = 0; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요 private final UnitPriceRefresher unitPriceRefresher; private final PlatformVWAPService platformVWAPService; private final OrderService orderService; - private final TradeRepository tradeRepository; - private final VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler; private final OrderPricePolicy orderPricePolicy; private final DeviationPricePolicy deviationPricePolicy; private final OrderVolumePolicy orderVolumePolicy; private final OrderWalletRepository orderWalletRepository; private final OrderAccountRepository accountExternalRepository; + + private final VWAPMetricsRecorder recorder; private String ticker; @@ -51,15 +49,14 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// //호가 정책 적용 this.unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker); - //최근 체결 내역 가져오기 - List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); +// //최근 체결 내역 가져오기 +// List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); // Platform 기반 가격 생성 (10개 이하, 10개 이상에 따른 가격 생성) - double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,trades,apiVWAP); - + double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,apiVWAP); + 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( @@ -74,15 +71,7 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// createOrderWithFallback(ticker,false, sellVolume, sellPrice); createOrderWithFallback(ticker,true, buyVolume, buyPrice); - - try { - TimeUnit.MICROSECONDS.sleep(100); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } -// vwaPerrorInJectionScheduler.enableInjection(); //에러 발생기 비활성화 - - /* DecimalFormat df = new DecimalFormat("#,##0.00"); +/* DecimalFormat df = new DecimalFormat("#,##0.00"); DecimalFormat dfv = new DecimalFormat("#,###.########"); //모니터링용 System.out.println("sellPrice = " + df.format(sellPrice)); @@ -92,8 +81,7 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// System.out.println("buyVolume = " + dfv.format(buyVolume)); System.out.println("===================================="); - System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP)); -*/ + System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP));*/ } /*System.out.println("📦"+ticker+" [체결 기록 Top 10]"); trades.forEach(t -> @@ -109,7 +97,7 @@ private void createOrderWithFallback(String ticker,boolean isBuy, double volume, new DecimalFormat("#,###.########").format(volume)); return; } - + recorder.recordPrice(ticker,isBuy,price); try { orderService.createOrderWithBot(ticker, isBuy, volume, price); } catch (IllegalArgumentException e) { diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java index b6544433..6aadb643 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java @@ -2,25 +2,65 @@ import com.cleanengine.coin.realitybot.domain.PlatformVWAPState; import com.cleanengine.coin.trade.entity.Trade; -import lombok.extern.slf4j.Slf4j; +import com.cleanengine.coin.trade.repository.TradeRepository; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Service -@Slf4j +@RequiredArgsConstructor public class PlatformVWAPService {//TODO 가상 시장 조회용 사라질 예정임 + private final TradeRepository tradeRepository; + Map vwapMap = new ConcurrentHashMap<>(); + Map lastTradeTimeMap = new ConcurrentHashMap<>(); + - public double calculateVWAPbyTrades(String ticker,List trades,double apiVWAP) { + public double calculateVWAPbyTrades(String ticker,double apiVWAP) { PlatformVWAPState state = vwapMap.computeIfAbsent(ticker, PlatformVWAPState::new); - if (trades.size() < 10){ - //체결 내역이 10개 이하일 경우 자체 계산 - return generateVWAP(apiVWAP); + LocalDateTime lastTradeTime = lastTradeTimeMap.get(ticker); + + //최근 체결 내역 가져오기 + List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); + + if ( trades.size() < 10){ + //체결 내역이 10개 이하일 경우 자체 계산 + return generateVWAP(apiVWAP); + } + LocalDateTime newestTime = trades.get(0).getTradeTime(); + if (lastTradeTime == null) { + lastTradeTimeMap.put(ticker, newestTime); + state.addTrades(trades); + return state.getVWAP(); + } + boolean containsSameTime = false; + for (Trade trade : trades) { + if (trade.getTradeTime().isEqual(lastTradeTime)) { + containsSameTime = true; + break; } + } + + if (!containsSameTime) { + trades = tradeRepository.findByTickerAndTradeTimeGreaterThanEqualOrderByTradeTimeDesc(ticker, lastTradeTime); + newestTime = trades.get(0).getTradeTime(); + lastTradeTimeMap.put(ticker, newestTime); + } + + //================= state.addTrades(trades); + + /*System.out.println("📦"+ticker+" [체결 기록]"); + state.addTrades(trades);trades.forEach(t -> + System.out.printf("🕒 %s | 가격: %.0f | 수량: %.8f | 매수: #%d ↔ 매도: #%d%n", + t.getTradeTime(), t.getPrice(), t.getSize(), t.getBuyUserId(), t.getSellUserId()) + );*/ + + return state.getVWAP(); } 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 523015a0..6ef0e471 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java @@ -17,13 +17,18 @@ public class DeviationPricePolicy { */ public AdjustPrice adjust(double platformSell,double platformBuy, double trendLineRate, double apiVWAP, double unitPrice){ - double deviation = Math.abs(trendLineRate); - - if (deviation <= 0.01){ + double deviation = Math.abs(trendLineRate);//음수값 보정 + if (deviation <= 0.017){ return new AdjustPrice(platformSell,platformBuy); } double weight = getCorrectionWeight(deviation); - double closeness = 0.5 + (weight * 0.3); // 보간 가중치: 0.7 ~ 1.0 -> 0.5 +// 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 targetVWAP = (trendLineRate > 0) //만약 closeness 를 0.5 입력시 중간값 // ? apiVWAP + (platformSell - apiVWAP) * closeness // 고평가 → platformSell(25000) → apiVWAP(16000) 사이 가중치 %로 유도 @@ -39,6 +44,8 @@ public AdjustPrice adjust(double platformSell,double platformBuy, double trendLi buyTarget = apiVWAP - (apiVWAP - platformBuy) * closeness; } +// double adjustedSell = normalizeToUnit(sellTarget,unitPrice); +// double adjustedBuy = normalizeToUnit(buyTarget,unitPrice); double adjustedSell = normalizeToUnit(interpolate(platformSell,sellTarget ,weight),unitPrice); double adjustedBuy = normalizeToUnit(interpolate(platformBuy,buyTarget ,weight),unitPrice); @@ -70,7 +77,8 @@ private double getCorrectionWeight(double deviation) { * 선형 보간 함수: platformPrice → apiVWAP 사이 보간 */ private double interpolate(double platformPrice, double apiVWAP, double weight) { - return platformPrice * (1 - weight) + apiVWAP * weight; + double interWeight = Math.max(1,weight*1.2); + return platformPrice * (1 - interWeight) + apiVWAP * interWeight; } private double normalizeToUnit(double price, double unitPrice) { return Math.round(price / unitPrice) * unitPrice; diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java index 3f43eacf..0c19925f 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java @@ -22,12 +22,19 @@ public double calculateVolume(double avgVolume, double trendLineRate, boolean is //편차에 따른 거래량 보정 3% -> 최대 2.5배 증가 double deviation = Math.abs(trendLineRate); //절댓값 반환 - if (deviation>0.01){ //1% 초과할 경우 - double power = deviation * 100; //0.03 -> 3% -// double multiplier = 1.0 + (power * 0.5); //2.5배 (max로 사용) - double multiplier = Math.pow(1.1,power); //2.5배 (max로 사용) - rawVolume *= multiplier; //강한 추세 -> 강한 보정 + double power = deviation * 100; //0.03 -> 3% + double multiplier; + if (deviation >= 0.1){//1% 초과할 경우 + multiplier = 1.0 + (power * 0.5); //2.5배 (max로 사용) + } else if (deviation >= 0.01){ + double baseline = 5.0-((deviation - 0.01)/0.09)*2.0; + multiplier = baseline + (power * 0.5); + } else { + multiplier = 1.0 + (power * 0.5); } +// double multiplier = Math.pow(1.2,power); //2.5배 (max로 사용) + rawVolume *= multiplier; //강한 추세 -> 강한 보정 + //매수-매도 비중 조정 if (deviation <=0.001) //0.1%일 경우 안정권 , 추가적인 보정 x @@ -48,6 +55,9 @@ private double volumeExpansion(double rawVolume){ if(resultVolume <= 0) { //Volume이 0이하일 경우 재 계산 resultVolume = Math.round(rawVolume * 10000000.0) / 10000000.0; + if(resultVolume <= 0){ + resultVolume = 0.0000001; + } } return resultVolume; } diff --git a/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java b/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java index a8854cc6..30bc3de2 100644 --- a/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java +++ b/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java @@ -23,4 +23,5 @@ public interface TradeRepository extends JpaRepository { List findByBuyUserIdAndTicker(Integer buyUserId, String ticker); List findBySellUserIdAndTicker(Integer sellUserId, String ticker); List findTop10ByTickerOrderByTradeTimeDesc(String ticker); + List findByTickerAndTradeTimeGreaterThanEqualOrderByTradeTimeDesc(String ticker, LocalDateTime lastTime); } diff --git a/src/main/resources/application-actuator.yml b/src/main/resources/application-actuator.yml new file mode 100644 index 00000000..167d9532 --- /dev/null +++ b/src/main/resources/application-actuator.yml @@ -0,0 +1,12 @@ +management: + endpoints: + web: + exposure: + include: prometheus,health,metrics + endpoint: + health: + show-details: always + prometheus: + metrics: + export: + enabled: true \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 6ec6fe43..d204d6a6 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -27,7 +27,4 @@ spring: frontend: url: "http://localhost:5173/callback" -bot-handler: - fixed-rate: 5000 # 5초마다 실행 - corn : "0 0 0 * * *" # 매일 자정마다 호가 - order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b1ca5d89..c67b4de9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,7 +27,7 @@ spring: allowed-origins: http://localhost:63342,http://localhost:8080,http://localhost:5500,http://localhost:5173,https://investfuture.my endpoints: public: - paths: /api/login,/api/asset,/api/oauth2,/api/healthcheck,/api/coin/realtime,/api/coin/min,/api/minute-ohlc,/v3/api-docs,/swagger,/swagger-ui,/swagger-ui.html,/swagger-resources,/webjars,/h2-console,/favicon.ico + paths: /api/login,/api/asset,/api/oauth2,/api/healthcheck,/api/coin/realtime,/api/coin/min,/api/minute-ohlc,/v3/api-docs,/swagger,/swagger-ui,/swagger-ui.html,/swagger-resources,/webjars,/h2-console,/favicon.ico,/actuator,/test websocket: paths: /api/coin/min,/api/coin/realtime,/api/coin/orderbook jwt: diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java index cfb7bf79..ef27e0bc 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java @@ -1,24 +1,42 @@ package com.cleanengine.coin.realitybot.api; +import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; 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.Spy; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.SpyBean; + +import java.util.Collections; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; -@SpringBootTest +@ExtendWith(MockitoExtension.class) public class RefresherRunnerTest { - @SpyBean + + @Spy + @InjectMocks private UnitPriceRefresher unitPriceRefresher; + @Mock + private AssetRepository assetRepository; + @Mock + private ApplicationArguments applicationArguments; + + @BeforeEach + public void setUp(){ + when(assetRepository.findAll()).thenReturn(Collections.emptyList()); + } @DisplayName("어플리케이션 실행 시 호가 단위 수집") @Test - public void runwithrefrecher(){ + public void runwithrefrecher() { + unitPriceRefresher.run(applicationArguments); + verify(unitPriceRefresher,times(1)).run(any(ApplicationArguments.class)); verify(unitPriceRefresher,times(1)).initializeUnitPrices(); } 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 b4138452..58105cc7 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java @@ -38,7 +38,7 @@ void testCalculateVWAPLessThan10Trades() { double apiVWAP = 1000.0; //0.1%의 보정값 //when - double result = platformVWAPService.calculateVWAPbyTrades(ticker, trades, apiVWAP); + double result = platformVWAPService.calculateVWAPbyTrades(ticker, apiVWAP); //than assertEquals(apiVWAP, result,1); @@ -67,7 +67,7 @@ void testCalculateVWAPMoreThan10Trades() { when(platformVwapState.getVWAP()).thenReturn(15000.0); platformVWAPService.vwapMap.put(ticker, platformVwapState); //when - double result = platformVWAPService.calculateVWAPbyTrades(ticker, trades, apiVWAP); + double result = platformVWAPService.calculateVWAPbyTrades(ticker, apiVWAP); //then verify(platformVwapState).addTrades(trades); diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java similarity index 60% rename from src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java rename to src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java index 059e24d1..6806f901 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java @@ -2,8 +2,6 @@ import com.cleanengine.coin.trade.entity.Trade; import com.cleanengine.coin.trade.repository.TradeRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @@ -12,23 +10,14 @@ import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; @Component -@RequiredArgsConstructor -public class VWAPerrorInJectionScheduler { - +public class VWAPErrorInjector { private final TradeRepository tradeRepository; - private boolean shouldInject = false; - private boolean hasInjected = false; - - public void enableInjection() { - if (hasInjected) return; // 이미 실행한 적 있으면 무시 - this.shouldInject = true; + public VWAPErrorInjector(TradeRepository tradeRepository) { + this.tradeRepository = tradeRepository; } - @Scheduled(fixedRate = 30000) // 혹은 따로 수동 호출도 가능 - public void injectFakeTrade() { - if (!shouldInject || hasInjected) return; - + public void injectErrorTrade(){ Trade fakeTrade = new Trade(); fakeTrade.setTicker("TRUMP"); fakeTrade.setBuyUserId(BUY_ORDER_BOT_ID); // 테스트용 유저 ID @@ -40,9 +29,5 @@ public void injectFakeTrade() { fakeTrade.setTradeTime(LocalDateTime.now()); tradeRepository.save(fakeTrade); - hasInjected = true; // ✅ 한 번 실행했으니 플래그 설정 - shouldInject = false; - - System.out.println("🚨 혼동 Trade 1건 삽입 완료!"); } } diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java index 2432d626..a5c884a8 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java @@ -21,20 +21,20 @@ public class VWAPerrorInJectionSchedulerTest { TradeRepository tradeRepository; @InjectMocks - VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler; + VWAPErrorInjector vwapErrorInjector; + @Test @DisplayName("enableInjection() 호출 전에는 작동 안한다") void doNotingInjection(){ - vwaPerrorInJectionScheduler.injectFakeTrade(); + vwapErrorInjector.injectErrorTrade(); verify(tradeRepository,never()).save(any()); } @Test @DisplayName("호출 후에 fateTrade 삽입") void injectOnceAfterEnable(){ - vwaPerrorInJectionScheduler.enableInjection(); - vwaPerrorInJectionScheduler.injectFakeTrade(); + vwapErrorInjector.injectErrorTrade(); ArgumentCaptor captor = ArgumentCaptor.forClass(Trade.class); verify(tradeRepository,times(1)).save(captor.capture()); @@ -42,18 +42,4 @@ void injectOnceAfterEnable(){ Trade trade = captor.getValue(); assertEquals("TRUMP",trade.getTicker()); } - @Test - @DisplayName("한 번 삽입 이후 재삽입 되지 않음") - void onlyOnecInject(){ - vwaPerrorInJectionScheduler.enableInjection(); - verify(tradeRepository, never()).save(any()); - vwaPerrorInJectionScheduler.injectFakeTrade(); - verify(tradeRepository,times(1)).save(any()); - - vwaPerrorInJectionScheduler.injectFakeTrade(); - vwaPerrorInJectionScheduler.enableInjection(); - verify(tradeRepository,times(1)).save(any()); - - } - } \ No newline at end of file