From d7d7d5eec55dcded5e9b5ae491ca51e305142d67 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Wed, 28 May 2025 04:48:44 +0900 Subject: [PATCH 01/31] =?UTF-8?q?feat:=20=ED=98=B8=EA=B0=80=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=EC=A0=95=EC=B1=85=20=EC=9E=91=EC=97=85=EC=A4=91=20?= =?UTF-8?q?issue:=20#91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/ApiScheduler.java | 2 +- .../coin/realitybot/api/BithumbAPIClient.java | 26 +++++-- .../realitybot/api/UnitPriceRefresher.java | 59 +++++++++++++++ .../coin/realitybot/dto/OpeningPrice.java | 25 +++++++ .../realitybot/parser/OpeningPriceParser.java | 71 +++++++++++++++++++ .../{service => parser}/TickParser.java | 2 +- .../service/OrderGenerateService.java | 6 +- .../coin/realitybot/vo/UnitPricePolicy.java | 36 ++++++++++ 8 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java rename src/main/java/com/cleanengine/coin/realitybot/{service => parser}/TickParser.java (94%) create mode 100644 src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java 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 660526a8..912a292c 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -6,7 +6,7 @@ import com.cleanengine.coin.realitybot.dto.Ticks; import com.cleanengine.coin.realitybot.service.ApiVWAPService; import com.cleanengine.coin.realitybot.service.OrderGenerateService; -import com.cleanengine.coin.realitybot.service.TickParser; +import com.cleanengine.coin.realitybot.parser.TickParser; import com.cleanengine.coin.realitybot.service.TickServiceManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index a0ee5f31..e33a3f7f 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -14,15 +14,17 @@ @RequiredArgsConstructor @Slf4j public class BithumbAPIClient { - private OkHttpClient client; - private Gson gson; +// private OkHttpClient client; +// private Gson gson; + private OkHttpClient client = new OkHttpClient(); +// private Gson gson = new Gson(); private String ticker; public String get(String ticker){ //API를 responseBody에 담아 반환 this.ticker = ticker; - client = new OkHttpClient(); - gson = new Gson(); +// client = new OkHttpClient(); +// gson = new Gson(); Request request = new Request.Builder() .url("https://api.bithumb.com/v1/trades/ticks?market=krw-"+ticker+"&count=10") .get() @@ -30,6 +32,22 @@ public String get(String ticker){ //API를 responseBody에 담아 반환 .build(); try (Response response = client.newCall(request).execute()){ String responseBody = response.body().string(); +// return gson.toJson(response.body().string()); + log.debug("{}의 Bithumb API 응답 : {}",ticker,responseBody); + return responseBody; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + public String getOpeningPirce(String ticker){ + this.ticker = ticker; + Request request = new Request.Builder() + .url("https://api.bithumb.com/v1/ticker?markets=KRW-"+ticker) + .get() + .addHeader("accept", "application/json") + .build(); + try (Response response = client.newCall(request).execute()){ + String responseBody = response.body().string(); // return gson.toJson(response.body().string()); log.debug("{}의 Bithumb API 응답 : {}",ticker,responseBody); return responseBody; diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java new file mode 100644 index 00000000..d21b8aa0 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java @@ -0,0 +1,59 @@ +package com.cleanengine.coin.realitybot.api; + +import com.cleanengine.coin.order.domain.Asset; +import com.cleanengine.coin.order.infra.AssetRepository; +import com.cleanengine.coin.realitybot.dto.OpeningPrice; +import com.cleanengine.coin.realitybot.parser.OpeningPriceParser; +import com.cleanengine.coin.realitybot.parser.TickParser; +import com.cleanengine.coin.realitybot.vo.UnitPricePolicy; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UnitPriceRefresher { + private final UnitPricePolicy unitPricePolicy; + private final AssetRepository assetRepository; + private final BithumbAPIClient bithumbAPIClient; + private final OpeningPriceParser openingPriceParser; + private final Map unitPriceCache = new ConcurrentHashMap<>(); + + @PostConstruct + public void initializeUnitPrices() { + List tickers = assetRepository.findAll(); + for (Asset ticker : tickers){ + double unitPrice = fetchOpeningPriceFromAPI(ticker.getName()); + System.out.println(unitPrice+"가 작동되었습니다 ==================="); + unitPriceCache.put(ticker.getName(),unitPrice); + } + } + +// @Scheduled(cron = "0 * * * * *") + public void refreshUnitPrices() { + unitPriceCache.keySet().forEach(ticker -> { + unitPriceCache.put(ticker, fetchOpeningPriceFromAPI(ticker)); + }); + + List tickers = assetRepository.findAll(); + for (Asset ticker : tickers){ + double unitPrice = fetchOpeningPriceFromAPI(ticker.getName()); + System.out.println(unitPrice+"가 작동되었습니다 ==================="); + unitPriceCache.put(ticker.getName(),unitPrice); + } + } + + private double fetchOpeningPriceFromAPI(String ticker) { + String rawJson = bithumbAPIClient.getOpeningPirce(ticker); //api raw데이터 + OpeningPrice json = openingPriceParser.parseGson(rawJson); //json을 list로 변환 + + return json.getOpeningPrice(); + } +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java b/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java new file mode 100644 index 00000000..af355e45 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java @@ -0,0 +1,25 @@ +package com.cleanengine.coin.realitybot.dto; + +import com.google.gson.annotations.SerializedName; +import lombok.*; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class OpeningPrice { + String market; + @SerializedName("opening_price") + double openingPrice; + @SerializedName("trade_price") + double tradePrice; + + @Override + public String toString() { + return "OpeningPrice{" + + "market='" + market + '\'' + + ", OpeningPrice=" + openingPrice + + ", tradePrice=" + tradePrice + + '}'; + } +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java b/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java new file mode 100644 index 00000000..268ea4fc --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java @@ -0,0 +1,71 @@ +package com.cleanengine.coin.realitybot.parser; + +import com.cleanengine.coin.realitybot.dto.OpeningPrice; +import com.cleanengine.coin.realitybot.dto.Ticks; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +@Getter +public class OpeningPriceParser { + private static final Gson gson = new Gson(); + + + public static OpeningPrice parseGson(String json) { + ApiResponse response = gson.fromJson(json, ApiResponse.class); + + // JSON 내부 구조: data 필드 안의 값을 OpeningPrice로 변환 + Data data = response.getData(); + + return OpeningPrice.builder() + .market(data.getMarket()) // null일 수 있음 + .openingPrice(Double.parseDouble(data.getOpeningPrice())) + .tradePrice(Double.parseDouble(data.getTradePrice())) + .build(); + } + + // 전체 응답 구조 + static class ApiResponse { + private String status; + private Data data; + + public Data getData() { + return data; + } + } + + // "data" 객체 내부 구조 + static class Data { + private String market; + + @SerializedName("opening_price") + private String openingPrice; + + @SerializedName("trade_price") + private String tradePrice; + + public String getMarket() { + return market; + } + + public String getOpeningPrice() { + return openingPrice; + } + + public String getTradePrice() { + return tradePrice; + } + } + + + +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/TickParser.java b/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java similarity index 94% rename from src/main/java/com/cleanengine/coin/realitybot/service/TickParser.java rename to src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java index c82f6e84..0e346718 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/TickParser.java +++ b/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java @@ -1,4 +1,4 @@ -package com.cleanengine.coin.realitybot.service; +package com.cleanengine.coin.realitybot.parser; import com.cleanengine.coin.realitybot.dto.Ticks; import com.google.gson.Gson; 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 45710ed6..7681d0bd 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -10,6 +10,7 @@ import com.cleanengine.coin.user.domain.Wallet; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import java.text.DecimalFormat; @@ -21,6 +22,7 @@ @Slf4j @Service +@Order(5) @RequiredArgsConstructor public class OrderGenerateService { private final int[] orderLevels = {1,2,3}; @@ -124,14 +126,10 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// //스위치 시켜야 할까? createOrderWithFallback(ticker,false, sellVolume,sellPrice); createOrderWithFallback(ticker,true, buyVolume,buyPrice); - // queueManager.addSellOrder(sellPrice, sellVolume); // queueManager.addBuyOrder(buyPrice, buyVolume); - } - - try { TimeUnit.MICROSECONDS.sleep(100); } catch (InterruptedException e) { diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java new file mode 100644 index 00000000..e32d5157 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java @@ -0,0 +1,36 @@ +package com.cleanengine.coin.realitybot.vo; + +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Service; + +import java.util.NavigableMap; +import java.util.TreeMap; + +@Service +public class UnitPricePolicy { + private final NavigableMap unitPriceRules = new TreeMap(); + + @PostConstruct + public void initRules() { + unitPriceRules.put(0.0001,0.00000001); + unitPriceRules.put(0.001,0.0000001); + unitPriceRules.put(0.01,0.000001); + unitPriceRules.put(0.1,0.00001); + unitPriceRules.put(1.0,0.0001); + unitPriceRules.put(10.0,0.001); + unitPriceRules.put(100.0,0.01); + unitPriceRules.put(1_000.0,0.1); + unitPriceRules.put(10_000.0,1.0); + unitPriceRules.put(100_000.0,10.0); + unitPriceRules.put(500_000.0,50.0); + unitPriceRules.put(1_000_000.0,100.0); + unitPriceRules.put(5_000_000.0,500.0); + unitPriceRules.put(10_000_000.0,1_000.0); + } + + public double getUnitPrice(double apiTradePrice){ + double unitprice =unitPriceRules.higherEntry(apiTradePrice).getValue(); + System.out.println("========================================"+unitprice); + return unitprice; + } +} From 4ff7e1102eeca0fa22b8b873517b379ae6ac9667 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Wed, 28 May 2025 14:41:29 +0900 Subject: [PATCH 02/31] =?UTF-8?q?feat:=20=ED=98=B8=EA=B0=80=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=EC=A0=95=EC=B1=85=20-=20=EC=97=85=EB=B9=84?= =?UTF-8?q?=ED=8A=B8=20=ED=98=B8=EA=B0=80=20=EC=A0=95=EC=B1=85=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EB=8B=A8=EA=B0=80=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?issue=20:=20#91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/BithumbAPIClient.java | 8 +-- .../realitybot/api/UnitPriceRefresher.java | 39 +++++++++------ .../coin/realitybot/dto/OpeningPrice.java | 18 +++---- .../realitybot/parser/OpeningPriceParser.java | 49 +------------------ .../service/OrderGenerateService.java | 28 +++++------ .../coin/realitybot/vo/UnitPricePolicy.java | 6 +-- 6 files changed, 56 insertions(+), 92 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index e33a3f7f..c093a4e3 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -4,9 +4,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import okhttp3.OkHttpClient; -import okhttp3.*; +import okhttp3.Request; +import okhttp3.Response; import org.springframework.stereotype.Component; -import com.google.gson.Gson; import java.io.IOException; @@ -33,7 +33,7 @@ public String get(String ticker){ //API를 responseBody에 담아 반환 try (Response response = client.newCall(request).execute()){ String responseBody = response.body().string(); // return gson.toJson(response.body().string()); - log.debug("{}의 Bithumb API 응답 : {}",ticker,responseBody); + log.info("{}의 Bithumb API 응답 : {}",ticker,responseBody); return responseBody; } catch (IOException e) { throw new RuntimeException(e); @@ -49,7 +49,7 @@ public String getOpeningPirce(String ticker){ try (Response response = client.newCall(request).execute()){ String responseBody = response.body().string(); // return gson.toJson(response.body().string()); - log.debug("{}의 Bithumb API 응답 : {}",ticker,responseBody); + log.info("{}의 OpeningPirce 응답 : {}",ticker,responseBody); return responseBody; } catch (IOException e) { throw new RuntimeException(e); 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 d21b8aa0..55a2f59a 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java @@ -4,11 +4,11 @@ import com.cleanengine.coin.order.infra.AssetRepository; import com.cleanengine.coin.realitybot.dto.OpeningPrice; import com.cleanengine.coin.realitybot.parser.OpeningPriceParser; -import com.cleanengine.coin.realitybot.parser.TickParser; import com.cleanengine.coin.realitybot.vo.UnitPricePolicy; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -19,41 +19,50 @@ @Slf4j @Service @RequiredArgsConstructor -public class UnitPriceRefresher { +public class UnitPriceRefresher implements ApplicationRunner { private final UnitPricePolicy unitPricePolicy; private final AssetRepository assetRepository; private final BithumbAPIClient bithumbAPIClient; private final OpeningPriceParser openingPriceParser; private final Map unitPriceCache = new ConcurrentHashMap<>(); - @PostConstruct + @Override + public void run(ApplicationArguments args){ + log.info("Running Unit Price Refresher..."); + initializeUnitPrices(); + } + public void initializeUnitPrices() { + List tickers = assetRepository.findAll(); for (Asset ticker : tickers){ - double unitPrice = fetchOpeningPriceFromAPI(ticker.getName()); - System.out.println(unitPrice+"가 작동되었습니다 ==================="); - unitPriceCache.put(ticker.getName(),unitPrice); + double unitPrice = fetchOpeningPriceFromAPI(ticker.getTicker()); + unitPriceCache.put(ticker.getTicker(),unitPrice); } } -// @Scheduled(cron = "0 * * * * *") + @Scheduled(cron = "30 * * * * *") public void refreshUnitPrices() { - unitPriceCache.keySet().forEach(ticker -> { - unitPriceCache.put(ticker, fetchOpeningPriceFromAPI(ticker)); - }); +// unitPriceCache.keySet().forEach(ticker -> { +// unitPriceCache.put(ticker, fetchOpeningPriceFromAPI(ticker)); +// }); List tickers = assetRepository.findAll(); for (Asset ticker : tickers){ - double unitPrice = fetchOpeningPriceFromAPI(ticker.getName()); - System.out.println(unitPrice+"가 작동되었습니다 ==================="); - unitPriceCache.put(ticker.getName(),unitPrice); + double unitPrice = fetchOpeningPriceFromAPI(ticker.getTicker()); + unitPriceCache.put(ticker.getTicker(),unitPrice); } + } private double fetchOpeningPriceFromAPI(String ticker) { String rawJson = bithumbAPIClient.getOpeningPirce(ticker); //api raw데이터 OpeningPrice json = openingPriceParser.parseGson(rawJson); //json을 list로 변환 + double unitprice = unitPricePolicy.getUnitPrice(json.getOpening_price()); + return unitprice; + } - return json.getOpeningPrice(); + public double getUnitPriceByTicker(String ticker){ + return unitPriceCache.get(ticker); } } diff --git a/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java b/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java index af355e45..fcdef752 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java +++ b/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java @@ -1,25 +1,25 @@ package com.cleanengine.coin.realitybot.dto; -import com.google.gson.annotations.SerializedName; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder @AllArgsConstructor @NoArgsConstructor public class OpeningPrice { - String market; - @SerializedName("opening_price") - double openingPrice; - @SerializedName("trade_price") - double tradePrice; + private String market; + private double opening_price; + private double trade_price; @Override public String toString() { return "OpeningPrice{" + "market='" + market + '\'' + - ", OpeningPrice=" + openingPrice + - ", tradePrice=" + tradePrice + + ", OpeningPrice=" + opening_price + + ", tradePrice=" + trade_price + '}'; } } diff --git a/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java b/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java index 268ea4fc..1f4b4676 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java +++ b/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java @@ -1,9 +1,7 @@ package com.cleanengine.coin.realitybot.parser; import com.cleanengine.coin.realitybot.dto.OpeningPrice; -import com.cleanengine.coin.realitybot.dto.Ticks; import com.google.gson.Gson; -import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -19,53 +17,10 @@ public class OpeningPriceParser { private static final Gson gson = new Gson(); - public static OpeningPrice parseGson(String json) { - ApiResponse response = gson.fromJson(json, ApiResponse.class); - - // JSON 내부 구조: data 필드 안의 값을 OpeningPrice로 변환 - Data data = response.getData(); - - return OpeningPrice.builder() - .market(data.getMarket()) // null일 수 있음 - .openingPrice(Double.parseDouble(data.getOpeningPrice())) - .tradePrice(Double.parseDouble(data.getTradePrice())) - .build(); - } - - // 전체 응답 구조 - static class ApiResponse { - private String status; - private Data data; - - public Data getData() { - return data; - } - } - - // "data" 객체 내부 구조 - static class Data { - private String market; - - @SerializedName("opening_price") - private String openingPrice; - - @SerializedName("trade_price") - private String tradePrice; - - public String getMarket() { - return market; - } - - public String getOpeningPrice() { - return openingPrice; - } - - public String getTradePrice() { - return tradePrice; - } + List list = gson.fromJson(json, new TypeToken>() {}.getType()); + return list.get(0); } - } 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 7681d0bd..62dd07df 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -4,6 +4,7 @@ import com.cleanengine.coin.order.application.OrderService; import com.cleanengine.coin.order.external.adapter.account.AccountExternalRepository; import com.cleanengine.coin.order.external.adapter.wallet.WalletExternalRepository; +import com.cleanengine.coin.realitybot.api.UnitPriceRefresher; import com.cleanengine.coin.trade.entity.Trade; import com.cleanengine.coin.trade.repository.TradeRepository; import com.cleanengine.coin.user.domain.Account; @@ -26,7 +27,8 @@ @RequiredArgsConstructor public class OrderGenerateService { private final int[] orderLevels = {1,2,3}; - private final int unitPrice = 10; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요 + private double unitPrice = 0; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요 + private final UnitPriceRefresher unitPriceRefresher; private final PlatformVWAPService platformVWAPService; private final OrderService orderService; private final TradeRepository tradeRepository; @@ -39,6 +41,9 @@ public class OrderGenerateService { public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//기준 주문금액, 주문량 받기 (tick당 계산되어 들어옴) this.ticker = ticker; + //호가 정책 적용 + this.unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker); + //최근 체결 내역 가져오기 List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); @@ -118,16 +123,11 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// } createOrderWithFallback(ticker,false, sellVolume,sellPrice); createOrderWithFallback(ticker,true, buyVolume,buyPrice); - -// queueManager.addSellOrder(sellPrice, sellVolume); -// queueManager.addBuyOrder(buyPrice, buyVolume); //Queue 추가 } else { //스위치 시켜야 할까? createOrderWithFallback(ticker,false, sellVolume,sellPrice); createOrderWithFallback(ticker,true, buyVolume,buyPrice); -// queueManager.addSellOrder(sellPrice, sellVolume); -// queueManager.addBuyOrder(buyPrice, buyVolume); } try { @@ -137,23 +137,23 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// } // vwaPerrorInJectionScheduler.enableInjection(); //에러 발생기 비활성화 - /* //모니터링용 - System.out.println("sellPrice = " + sellPrice); + DecimalFormat df = new DecimalFormat("#,##0.00"); + //모니터링용 + System.out.println("sellPrice = " + df.format(sellPrice)); System.out.println("sellVolume = " + sellVolume); //모니터링용 - System.out.println("buyPrice = " + buyPrice); + System.out.println("buyPrice = " + df.format(buyPrice)); System.out.println("buyVolume = " + buyVolume); System.out.println("===================================="); - DecimalFormat df = new DecimalFormat("#,##0.00"); - 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]"); + System.out.println("📦"+ticker+" [체결 기록 Top 10]"); trades.forEach(t -> System.out.printf("🕒 %s | 가격: %.0f | 수량: %.8f | 매수: #%d ↔ 매도: #%d%n", t.getTradeTime(), t.getPrice(), t.getSize(), t.getBuyUserId(), t.getSellUserId()) - );*/ + ); } private void createOrderWithFallback(String ticker,boolean isBuy, double volume, double price ) { @@ -210,7 +210,7 @@ private double getDynamicMaxRate(double trendLineRate) { } private int normalizeToUnit(double price){ //호가단위로 변환 - return (int)(Math.round(price / unitPrice)) * unitPrice; + return (int) ((double)(Math.round(price / unitPrice)) * unitPrice); } private double getRandomVolum(double avgVolum){ //볼륨 랜덤 입력 double rawVolume = avgVolum * (0.5+Math.random()); diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java index e32d5157..23616910 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java @@ -24,13 +24,13 @@ public void initRules() { unitPriceRules.put(100_000.0,10.0); unitPriceRules.put(500_000.0,50.0); unitPriceRules.put(1_000_000.0,100.0); - unitPriceRules.put(5_000_000.0,500.0); - unitPriceRules.put(10_000_000.0,1_000.0); + unitPriceRules.put(2_000_000.0,500.0); + unitPriceRules.put(Double.MAX_VALUE,1_000.0); } public double getUnitPrice(double apiTradePrice){ double unitprice =unitPriceRules.higherEntry(apiTradePrice).getValue(); - System.out.println("========================================"+unitprice); return unitprice; } + } From 28e48267b07579b246a102533f0ee0d43d00b815 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Wed, 28 May 2025 16:15:23 +0900 Subject: [PATCH 03/31] =?UTF-8?q?feat:=20=EC=A3=BC=EB=AC=B8=20=EA=B0=95?= =?UTF-8?q?=EB=8F=84=20=EC=A1=B0=EC=A0=88=20-=20fixed-rate=20:=205?= =?UTF-8?q?=EC=B4=88=EB=A7=88=EB=8B=A4=20=EC=A3=BC=EB=AC=B8=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20-=20order-level=20:=20=ED=95=9C=EB=B2=88=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=8B=9C=20=EC=88=AB=EC=9E=90=20=EA=B0=AF?= =?UTF-8?q?=EC=88=98=EB=A7=8C=ED=81=BC=20=EC=8B=A4=ED=96=89=20=EB=B0=8F=20?= =?UTF-8?q?=EC=98=A4=EB=8D=94=EB=B6=81=EC=97=90=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=A3=BC=EB=AC=B8=20-=20corn=20:=20?= =?UTF-8?q?=EB=8B=A8=EA=B0=80=20=EC=88=98=EC=A7=91=20=EA=B8=B0=EA=B0=84,?= =?UTF-8?q?=20=EC=A3=BC=EB=AC=B8=20=EA=B0=80=EA=B2=A9=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20issue=20:=20#93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/ApiScheduler.java | 3 +- .../realitybot/config/SchedulerConfig.java | 35 +++++++++++++++++++ .../service/OrderGenerateService.java | 7 ++-- src/main/resources/application.yml | 7 +++- 4 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java 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 912a292c..a586075d 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -10,7 +10,6 @@ import com.cleanengine.coin.realitybot.service.TickServiceManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; @@ -31,7 +30,7 @@ public class ApiScheduler { private final AssetRepository assetRepository; private String ticker; - @Scheduled(fixedRate = 5000) +// @Scheduled(fixedRate = 5000) public void MarketAllRequest() throws InterruptedException { List tickers = assetRepository.findAll(); for (Asset ticker : tickers){ diff --git a/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java b/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java new file mode 100644 index 00000000..9972c5f5 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java @@ -0,0 +1,35 @@ +package com.cleanengine.coin.realitybot.config; + +import com.cleanengine.coin.realitybot.api.ApiScheduler; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +@Configuration +@EnableScheduling +@RequiredArgsConstructor +public class SchedulerConfig implements SchedulingConfigurer { + + //멀티쓰레드 환경 x +// @Autowired +// private TaskScheduler apiScheduler; + private final ApiScheduler apiScheduler; + + @Value("${bot-handler.fixed-rate}") + private long fixedRate; + + @Override + public void configureTasks(ScheduledTaskRegistrar registrar) { +// registrar.setScheduler(apiScheduler); //멀티 쓰레드 x + registrar.addFixedRateTask(()-> { + try { + apiScheduler.MarketAllRequest(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + },fixedRate); + } +} 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 62dd07df..9eb859ba 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -11,6 +11,7 @@ import com.cleanengine.coin.user.domain.Wallet; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; @@ -26,7 +27,8 @@ @Order(5) @RequiredArgsConstructor public class OrderGenerateService { - private final int[] orderLevels = {1,2,3}; + @Value("${bot-handler.order-level}") + private int[] orderLevels; //체결 강도 private double unitPrice = 0; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요 private final UnitPriceRefresher unitPriceRefresher; private final PlatformVWAPService platformVWAPService; @@ -212,10 +214,11 @@ private double getDynamicMaxRate(double trendLineRate) { private int normalizeToUnit(double price){ //호가단위로 변환 return (int) ((double)(Math.round(price / unitPrice)) * unitPrice); } + private double getRandomVolum(double avgVolum){ //볼륨 랜덤 입력 double rawVolume = avgVolum * (0.5+Math.random()); //호가 단위에 따라 0원이 발생 가능성 - double resultVolume = Math.round(rawVolume * 1000.0)/1000.0; + double resultVolume = Math.round(rawVolume * 10000.0)/10000.0; if(resultVolume <= 0){ //Volume이 0이하일 경우 재 계산 resultVolume = Math.round(rawVolume * 10000000.0)/10000000.0; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 18466b76..7d298092 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -42,4 +42,9 @@ spring: order: tickers: BTC, TRUMP server: - forward-headers-strategy: native \ No newline at end of file + forward-headers-strategy: native + +bot-handler: + fixed-rate: 5000 # 5초마다 실행 + corn : "0 0 0 * * *" # 매일 자정마다 호가 + order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 From 05d944dae5c8475a6ecf0b8b87c1769bd5338b4b Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Wed, 28 May 2025 17:41:26 +0900 Subject: [PATCH 04/31] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/BithumbAPIClient.java | 4 ++-- .../realitybot/service/OrderGenerateService.java | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index c093a4e3..605f6e34 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -33,7 +33,7 @@ public String get(String ticker){ //API를 responseBody에 담아 반환 try (Response response = client.newCall(request).execute()){ String responseBody = response.body().string(); // return gson.toJson(response.body().string()); - log.info("{}의 Bithumb API 응답 : {}",ticker,responseBody); + log.debug("{}의 Bithumb API 응답 : {}",ticker,responseBody); return responseBody; } catch (IOException e) { throw new RuntimeException(e); @@ -49,7 +49,7 @@ public String getOpeningPirce(String ticker){ try (Response response = client.newCall(request).execute()){ String responseBody = response.body().string(); // return gson.toJson(response.body().string()); - log.info("{}의 OpeningPirce 응답 : {}",ticker,responseBody); + log.debug("{}의 OpeningPirce 응답 : {}",ticker,responseBody); return responseBody; } catch (IOException e) { throw new RuntimeException(e); 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 9eb859ba..82ab9253 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -139,23 +139,24 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// } // 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)); - System.out.println("sellVolume = " + sellVolume); + System.out.println("sellVolume = " + dfv.format(sellVolume)); //모니터링용 System.out.println("buyPrice = " + df.format(buyPrice)); - System.out.println("buyVolume = " + buyVolume); + 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]"); + /*System.out.println("📦"+ticker+" [체결 기록 Top 10]"); trades.forEach(t -> System.out.printf("🕒 %s | 가격: %.0f | 수량: %.8f | 매수: #%d ↔ 매도: #%d%n", t.getTradeTime(), t.getPrice(), t.getSize(), t.getBuyUserId(), t.getSellUserId()) - ); + );*/ } private void createOrderWithFallback(String ticker,boolean isBuy, double volume, double price ) { From 7e0170bc9831f9c29cbc6be120699d17b705886b Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Thu, 29 May 2025 17:53:31 +0900 Subject: [PATCH 05/31] =?UTF-8?q?add:=20=EC=A3=BC=EC=84=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/BithumbAPIClient.java | 5 +- .../realitybot/api/UnitPriceRefresher.java | 5 +- .../realitybot/config/SchedulerConfig.java | 8 +- src/main/resources/application.yml | 2 +- .../realitybot/api/BithumbAPIClientTest.java | 84 +++++++++++++++++++ .../api/UnitPriceRefresherTest.java | 27 ++++++ .../realitybot/vo/UnitPricePolicyTest.java | 20 +++++ 7 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index 605f6e34..6c54ebc2 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -14,10 +14,7 @@ @RequiredArgsConstructor @Slf4j public class BithumbAPIClient { -// private OkHttpClient client; -// private Gson gson; private OkHttpClient client = new OkHttpClient(); -// private Gson gson = new Gson(); private String ticker; @@ -39,7 +36,7 @@ public String get(String ticker){ //API를 responseBody에 담아 반환 throw new RuntimeException(e); } } - public String getOpeningPirce(String ticker){ + public String getOpeningPrice(String ticker){ this.ticker = ticker; Request request = new Request.Builder() .url("https://api.bithumb.com/v1/ticker?markets=KRW-"+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 55a2f59a..fa55a6cf 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java @@ -33,7 +33,6 @@ public void run(ApplicationArguments args){ } public void initializeUnitPrices() { - List tickers = assetRepository.findAll(); for (Asset ticker : tickers){ double unitPrice = fetchOpeningPriceFromAPI(ticker.getTicker()); @@ -41,7 +40,7 @@ public void initializeUnitPrices() { } } - @Scheduled(cron = "30 * * * * *") + @Scheduled(cron = "${bot-handler.corn}") public void refreshUnitPrices() { // unitPriceCache.keySet().forEach(ticker -> { // unitPriceCache.put(ticker, fetchOpeningPriceFromAPI(ticker)); @@ -56,7 +55,7 @@ public void refreshUnitPrices() { } private double fetchOpeningPriceFromAPI(String ticker) { - String rawJson = bithumbAPIClient.getOpeningPirce(ticker); //api raw데이터 + String rawJson = bithumbAPIClient.getOpeningPrice(ticker); //api raw데이터 OpeningPrice json = openingPriceParser.parseGson(rawJson); //json을 list로 변환 double unitprice = unitPricePolicy.getUnitPrice(json.getOpening_price()); return unitprice; diff --git a/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java b/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java index 9972c5f5..1dcef296 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java +++ b/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java @@ -8,6 +8,8 @@ import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import java.time.Duration; + @Configuration @EnableScheduling @RequiredArgsConstructor @@ -19,17 +21,17 @@ public class SchedulerConfig implements SchedulingConfigurer { private final ApiScheduler apiScheduler; @Value("${bot-handler.fixed-rate}") - private long fixedRate; + private Duration fixedRate; @Override public void configureTasks(ScheduledTaskRegistrar registrar) { // registrar.setScheduler(apiScheduler); //멀티 쓰레드 x - registrar.addFixedRateTask(()-> { + registrar.addFixedRateTask(() -> { try { apiScheduler.MarketAllRequest(); } catch (InterruptedException e) { throw new RuntimeException(e); } - },fixedRate); + }, fixedRate); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7d298092..41878e3a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,5 +46,5 @@ server: bot-handler: fixed-rate: 5000 # 5초마다 실행 - corn : "0 0 0 * * *" # 매일 자정마다 호가 + corn : "30 * * * * *" # 매일 자정마다 호가 order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java new file mode 100644 index 00000000..2ffe2ba4 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java @@ -0,0 +1,84 @@ +package com.cleanengine.coin.realitybot.api; + +import okhttp3.*; +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.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +@ExtendWith(MockitoExtension.class) +class BithumbAPIClientTest { + + @Mock + private OkHttpClient client; + @Mock + private Call call; + @InjectMocks + BithumbAPIClient bithumbAPIClient; + + @Test + void get() { + } + + @DisplayName("실행시 API의 response에 Opening_price 값이 들어오는 지") + @Test + void getOpeningPrice() throws IOException { + //given + String ticker = "BTC"; + String json = "{\n" + + " \"market\": \"KRW-BTC\",\n" + + " \"trade_date\": \"20180418\",\n" + + " \"trade_time\": \"102340\",\n" + + " \"trade_date_kst\": \"20180418\",\n" + + " \"trade_time_kst\": \"192340\",\n" + + " \"trade_timestamp\": 1524047020000,\n" + + " \"opening_price\": 8450000,\n" + + " \"high_price\": 8679000,\n" + + " \"low_price\": 8445000,\n" + + " \"trade_price\": 8621000,\n" + + " \"prev_closing_price\": 8450000,\n" + + " \"change\": \"RISE\",\n" + + " \"change_price\": 171000,\n" + + " \"change_rate\": 0.0202366864,\n" + + " \"signed_change_price\": 171000,\n" + + " \"signed_change_rate\": 0.0202366864,\n" + + " \"trade_volume\": 0.02467802,\n" + + " \"acc_trade_price\": 108024804862.58253,\n" + + " \"acc_trade_price_24h\": 232702901371.09308,\n" + + " \"acc_trade_volume\": 12603.53386105,\n" + + " \"acc_trade_volume_24h\": 27181.31137002,\n" + + " \"highest_52_week_price\": 28885000,\n" + + " \"highest_52_week_date\": \"2018-01-06\",\n" + + " \"lowest_52_week_price\": 4175000,\n" + + " \"lowest_52_week_date\": \"2017-09-25\",\n" + + " \"timestamp\": 1524047026072\n" + + " }"; + ResponseBody responseBody = ResponseBody.create(json, MediaType.get("application/json")); + Request mockrequest = new Request.Builder().url("http://localhost").build(); + Response mockresponse = new Response.Builder() + .request(mockrequest) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(responseBody) + .build(); + + when(client.newCall(any())).thenReturn(call); + when(call.execute()).thenReturn(mockresponse); + + //when + String response = bithumbAPIClient.getOpeningPrice(ticker); + + //then + assertTrue(response.contains("opening_price")); + assertEquals(json, response); + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java new file mode 100644 index 00000000..e8513a67 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java @@ -0,0 +1,27 @@ +package com.cleanengine.coin.realitybot.api; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class UnitPriceRefresherTest { + + @DisplayName("작동 시작시") + @Test + void run() { + } + + @DisplayName("run 시작시 ") + @Test + void initializeUnitPrices() { + } + + @Test + void refreshUnitPrices() { + } + + @Test + void getUnitPriceByTicker() { + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java b/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java new file mode 100644 index 00000000..1f6060eb --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java @@ -0,0 +1,20 @@ +package com.cleanengine.coin.realitybot.vo; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class UnitPricePolicyTest { + + private UnitPricePolicy unitPricePolicy; + + @BeforeEach + void setUp(){ + unitPricePolicy = new UnitPricePolicy(); + unitPricePolicy.initRules(); + } + + @Test + void getUnitPrice(){ + assertEquals(); + } +} \ No newline at end of file From 20502ab9a39b404a4cc75253a50d56d1af20a966 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Fri, 30 May 2025 04:43:56 +0900 Subject: [PATCH 06/31] =?UTF-8?q?test:=20=ED=98=B8=EA=B0=80=20=EC=88=98?= =?UTF-8?q?=EC=A7=91=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 22 +++++++ .../realitybot/api/UnitPriceRefresher.java | 15 ++--- .../coin/realitybot/dto/OpeningPrice.java | 6 +- .../realitybot/parser/OpeningPriceParser.java | 4 +- .../coin/realitybot/vo/APITicker.java | 14 ----- .../coin/realitybot/vo/UnitPricePolicy.java | 9 ++- .../api/UnitPriceRefresherTest.java | 58 +++++++++++++++---- .../coin/realitybot/dto/OpeningPriceTest.java | 54 +++++++++++++++++ .../realitybot/vo/UnitPricePolicyTest.java | 27 ++++++++- 9 files changed, 165 insertions(+), 44 deletions(-) delete mode 100644 src/main/java/com/cleanengine/coin/realitybot/vo/APITicker.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/dto/OpeningPriceTest.java diff --git a/build.gradle b/build.gradle index 9154308d..ef21ea0e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.5' id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' //추가함 } group = 'com.cleanengine' @@ -23,6 +24,26 @@ repositories { mavenCentral() } +jacoco { //추가함 + toolVersion = "0.8.13" +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + html.required = true + } + + afterEvaluate { + getClassDirectories().setFrom(files(classDirectories.files.collect{ + fileTree(dir : it, includes: [ + "com/cleanengine/coin/realitybot/**" + ]) + })) + } +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-websocket' // WS + STOMP @@ -63,4 +84,5 @@ dependencies { tasks.named('test') { useJUnitPlatform() + finalizedBy jacocoTestReport //추가함 } 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 fa55a6cf..49074ec9 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java @@ -42,15 +42,12 @@ public void initializeUnitPrices() { @Scheduled(cron = "${bot-handler.corn}") public void refreshUnitPrices() { -// unitPriceCache.keySet().forEach(ticker -> { -// unitPriceCache.put(ticker, fetchOpeningPriceFromAPI(ticker)); -// }); - - List tickers = assetRepository.findAll(); - for (Asset ticker : tickers){ - double unitPrice = fetchOpeningPriceFromAPI(ticker.getTicker()); - unitPriceCache.put(ticker.getTicker(),unitPrice); - } + initializeUnitPrices(); +// List tickers = assetRepository.findAll(); +// for (Asset ticker : tickers){ +// double unitPrice = fetchOpeningPriceFromAPI(ticker.getTicker()); +// unitPriceCache.put(ticker.getTicker(),unitPrice); +// } } diff --git a/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java b/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java index fcdef752..c14313e1 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java +++ b/src/main/java/com/cleanengine/coin/realitybot/dto/OpeningPrice.java @@ -1,11 +1,9 @@ package com.cleanengine.coin.realitybot.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Getter +@Setter @Builder @AllArgsConstructor @NoArgsConstructor diff --git a/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java b/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java index 1f4b4676..5c4328a4 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java +++ b/src/main/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParser.java @@ -15,9 +15,9 @@ @Slf4j @Getter public class OpeningPriceParser { - private static final Gson gson = new Gson(); + private final Gson gson = new Gson(); - public static OpeningPrice parseGson(String json) { + public OpeningPrice parseGson(String json) { List list = gson.fromJson(json, new TypeToken>() {}.getType()); return list.get(0); } diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/APITicker.java b/src/main/java/com/cleanengine/coin/realitybot/vo/APITicker.java deleted file mode 100644 index 04458350..00000000 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/APITicker.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.cleanengine.coin.realitybot.vo; - -import lombok.Getter; - -@Getter -public enum APITicker { - TRUMP("TRUMP"), BTC("BTC"); - - private final String name; - APITicker(String name) { - this.name = name; - } - -} diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java index 23616910..59939a66 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicy.java @@ -1,11 +1,13 @@ package com.cleanengine.coin.realitybot.vo; import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.NavigableMap; import java.util.TreeMap; +@Slf4j @Service public class UnitPricePolicy { private final NavigableMap unitPriceRules = new TreeMap(); @@ -29,8 +31,11 @@ public void initRules() { } public double getUnitPrice(double apiTradePrice){ - double unitprice =unitPriceRules.higherEntry(apiTradePrice).getValue(); - return unitprice; + if (apiTradePrice <=0){ + log.warn("api의 opening_price가 음수입니다. 0원으로 치환됩니다."); + return unitPriceRules.higherEntry(0.0).getValue(); + } + return unitPriceRules.higherEntry(apiTradePrice).getValue(); } } diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java index e8513a67..3111096f 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java @@ -1,27 +1,63 @@ package com.cleanengine.coin.realitybot.api; +import com.cleanengine.coin.order.domain.Asset; +import com.cleanengine.coin.order.infra.AssetRepository; +import com.cleanengine.coin.realitybot.dto.OpeningPrice; +import com.cleanengine.coin.realitybot.parser.OpeningPriceParser; +import com.cleanengine.coin.realitybot.vo.UnitPricePolicy; 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 org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.List; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; +@ExtendWith(MockitoExtension.class) class UnitPriceRefresherTest { + @InjectMocks + private UnitPriceRefresher unitPriceRefresher; - @DisplayName("작동 시작시") - @Test - void run() { - } + @Mock + private AssetRepository assetRepository; + + @Mock + private BithumbAPIClient bithumbAPIClient; + + @Mock + private OpeningPriceParser openingPriceParser; + + @Mock + private UnitPricePolicy unitPricePolicy; @DisplayName("run 시작시 ") @Test - void initializeUnitPrices() { - } + void testRefresherUnitPrice() { + //given + String ticker = "BTC"; + Asset btc = new Asset(ticker,"비트코인",null); - @Test - void refreshUnitPrices() { - } + String json = "[{\"market\": \"KRW-BTC\", \"opening_price\": 1000000, \"trade_price\": 1010000}]"; + OpeningPrice parsed = new OpeningPrice(); + parsed.setOpening_price(1000000); - @Test - void getUnitPriceByTicker() { + when(assetRepository.findAll()).thenReturn(List.of(btc)); + when(bithumbAPIClient.getOpeningPrice(ticker)).thenReturn(json); + when(openingPriceParser.parseGson(json)).thenReturn(parsed); + when(unitPricePolicy.getUnitPrice(1000000)).thenReturn(100.0); + //when + unitPriceRefresher.refreshUnitPrices(); + + //then + double unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker); + assertEquals(100.0, unitPrice); } + } \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/dto/OpeningPriceTest.java b/src/test/java/com/cleanengine/coin/realitybot/dto/OpeningPriceTest.java new file mode 100644 index 00000000..5073bfa7 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/dto/OpeningPriceTest.java @@ -0,0 +1,54 @@ +package com.cleanengine.coin.realitybot.dto; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class OpeningPriceTest { + + OpeningPrice openingPrice = new OpeningPrice(); + + @Test + void testGsonParsing(){ + String json = "{\n" + + " \"market\": \"KRW-BTC\",\n" + + " \"trade_date\": \"20180418\",\n" + + " \"trade_time\": \"102340\",\n" + + " \"trade_date_kst\": \"20180418\",\n" + + " \"trade_time_kst\": \"192340\",\n" + + " \"trade_timestamp\": 1524047020000,\n" + + " \"opening_price\": 8450000,\n" + + " \"high_price\": 8679000,\n" + + " \"low_price\": 8445000,\n" + + " \"trade_price\": 8621000,\n" + + " \"prev_closing_price\": 8450000,\n" + + " \"change\": \"RISE\",\n" + + " \"change_price\": 171000,\n" + + " \"change_rate\": 0.0202366864,\n" + + " \"signed_change_price\": 171000,\n" + + " \"signed_change_rate\": 0.0202366864,\n" + + " \"trade_volume\": 0.02467802,\n" + + " \"acc_trade_price\": 108024804862.58253,\n" + + " \"acc_trade_price_24h\": 232702901371.09308,\n" + + " \"acc_trade_volume\": 12603.53386105,\n" + + " \"acc_trade_volume_24h\": 27181.31137002,\n" + + " \"highest_52_week_price\": 28885000,\n" + + " \"highest_52_week_date\": \"2018-01-06\",\n" + + " \"lowest_52_week_price\": 4175000,\n" + + " \"lowest_52_week_date\": \"2017-09-25\",\n" + + " \"timestamp\": 1524047026072\n" + + " }"; + + Gson gson = new Gson(); + openingPrice = gson.fromJson(json, OpeningPrice.class); + String actual = openingPrice.toString(); + String expected = "OpeningPrice{market='KRW-BTC', OpeningPrice=8450000.0, tradePrice=8621000.0}"; + + assertEquals("KRW-BTC", openingPrice.getMarket()); + assertEquals(8450000, openingPrice.getOpening_price()); + assertEquals(8621000,openingPrice.getTrade_price()); + + assertEquals(expected, actual); + } + +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java b/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java index 1f6060eb..4d434a71 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java @@ -1,8 +1,13 @@ package com.cleanengine.coin.realitybot.vo; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + class UnitPricePolicyTest { private UnitPricePolicy unitPricePolicy; @@ -13,8 +18,26 @@ void setUp(){ unitPricePolicy.initRules(); } + + @DisplayName("opening_price가 단위 가격이 정확이 매핑되는 지 테스트") + @Test + void testGetUnitPrice(){ + + //then + //0원이 입력 될 경우 + assertEquals(0.00000001,unitPricePolicy.getUnitPrice(0)); + + //최저가 보다 낮은 금액을 입력했을 때 + assertEquals(0.00000001,unitPricePolicy.getUnitPrice(0.0000999)); + assertEquals(0.0000001,unitPricePolicy.getUnitPrice(0.000999)); + + //최고가 보다 높은 금액을 입력했을 때 + assertEquals(500,unitPricePolicy.getUnitPrice(1_999_999.9999999)); + assertEquals(1_000,unitPricePolicy.getUnitPrice(2_000_000.0000009)); + } + @DisplayName("opening_price는 음수보다 높아야 합니다.") @Test - void getUnitPrice(){ - assertEquals(); + void testNegativeValueThrowsException(){ + assertEquals(0.00000001,unitPricePolicy.getUnitPrice(-15)); } } \ No newline at end of file From 5cd8d40c4359c4122979b989082efcda70b15b92 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Fri, 30 May 2025 17:44:40 +0900 Subject: [PATCH 07/31] =?UTF-8?q?config:=20junit=20realitybot=EB=A7=8C=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ .../realitybot/RealitybotCoreTestSuite.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java diff --git a/build.gradle b/build.gradle index ef21ea0e..95581b5f 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,8 @@ dependencies { testImplementation 'org.testcontainers:mariadb' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.junit.platform:junit-platform-suite:1.10.0' //suite 기능 추가용 + // Spring Security + OAuth2 implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' diff --git a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java new file mode 100644 index 00000000..dcc08e85 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java @@ -0,0 +1,20 @@ +package com.cleanengine.coin.realitybot; + +import com.cleanengine.coin.realitybot.api.BithumbAPIClientTest; +import com.cleanengine.coin.realitybot.api.RefresherRunnerTest; +import com.cleanengine.coin.realitybot.api.UnitPriceRefresherTest; +import com.cleanengine.coin.realitybot.dto.OpeningPriceTest; +import com.cleanengine.coin.realitybot.vo.UnitPricePolicyTest; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +@Suite +@SelectClasses({ + RefresherRunnerTest.class, + BithumbAPIClientTest.class, + UnitPriceRefresherTest.class, + OpeningPriceTest.class, + UnitPricePolicyTest.class +}) +public class RealitybotCoreTestSuite { +} From cc32af4289e3f9597ae0eaeb01800d2b50ede067 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Fri, 30 May 2025 17:45:41 +0900 Subject: [PATCH 08/31] =?UTF-8?q?refactor:=20api=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=A4=91=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/BithumbAPIClient.java | 5 +- .../realitybot/api/BithumbAPIClientTest.java | 105 ++++++++++++------ 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index 6c54ebc2..727680b4 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -44,12 +44,15 @@ public String getOpeningPrice(String ticker){ .addHeader("accept", "application/json") .build(); try (Response response = client.newCall(request).execute()){ + if ((response.code() == 200)){ + log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker); + } String responseBody = response.body().string(); // return gson.toJson(response.body().string()); log.debug("{}의 OpeningPirce 응답 : {}",ticker,responseBody); return responseBody; } catch (IOException e) { - throw new RuntimeException(e); + throw new RuntimeException("API 요청 중 예외 발생",e); } } diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java index 2ffe2ba4..eb9ba170 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java @@ -10,12 +10,11 @@ import java.io.IOException; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class BithumbAPIClientTest { +public class BithumbAPIClientTest { @Mock private OkHttpClient client; @@ -27,46 +26,47 @@ class BithumbAPIClientTest { @Test void get() { } + private String ticker = "BTC"; + private String json = "{\n" + + " \"market\": \"KRW-BTC\",\n" + + " \"trade_date\": \"20180418\",\n" + + " \"trade_time\": \"102340\",\n" + + " \"trade_date_kst\": \"20180418\",\n" + + " \"trade_time_kst\": \"192340\",\n" + + " \"trade_timestamp\": 1524047020000,\n" + + " \"opening_price\": 8450000,\n" + + " \"high_price\": 8679000,\n" + + " \"low_price\": 8445000,\n" + + " \"trade_price\": 8621000,\n" + + " \"prev_closing_price\": 8450000,\n" + + " \"change\": \"RISE\",\n" + + " \"change_price\": 171000,\n" + + " \"change_rate\": 0.0202366864,\n" + + " \"signed_change_price\": 171000,\n" + + " \"signed_change_rate\": 0.0202366864,\n" + + " \"trade_volume\": 0.02467802,\n" + + " \"acc_trade_price\": 108024804862.58253,\n" + + " \"acc_trade_price_24h\": 232702901371.09308,\n" + + " \"acc_trade_volume\": 12603.53386105,\n" + + " \"acc_trade_volume_24h\": 27181.31137002,\n" + + " \"highest_52_week_price\": 28885000,\n" + + " \"highest_52_week_date\": \"2018-01-06\",\n" + + " \"lowest_52_week_price\": 4175000,\n" + + " \"lowest_52_week_date\": \"2017-09-25\",\n" + + " \"timestamp\": 1524047026072\n" + + " }"; + private String failJson = "{}"; @DisplayName("실행시 API의 response에 Opening_price 값이 들어오는 지") @Test - void getOpeningPrice() throws IOException { + void callOpeningPrice() throws IOException { //given - String ticker = "BTC"; - String json = "{\n" + - " \"market\": \"KRW-BTC\",\n" + - " \"trade_date\": \"20180418\",\n" + - " \"trade_time\": \"102340\",\n" + - " \"trade_date_kst\": \"20180418\",\n" + - " \"trade_time_kst\": \"192340\",\n" + - " \"trade_timestamp\": 1524047020000,\n" + - " \"opening_price\": 8450000,\n" + - " \"high_price\": 8679000,\n" + - " \"low_price\": 8445000,\n" + - " \"trade_price\": 8621000,\n" + - " \"prev_closing_price\": 8450000,\n" + - " \"change\": \"RISE\",\n" + - " \"change_price\": 171000,\n" + - " \"change_rate\": 0.0202366864,\n" + - " \"signed_change_price\": 171000,\n" + - " \"signed_change_rate\": 0.0202366864,\n" + - " \"trade_volume\": 0.02467802,\n" + - " \"acc_trade_price\": 108024804862.58253,\n" + - " \"acc_trade_price_24h\": 232702901371.09308,\n" + - " \"acc_trade_volume\": 12603.53386105,\n" + - " \"acc_trade_volume_24h\": 27181.31137002,\n" + - " \"highest_52_week_price\": 28885000,\n" + - " \"highest_52_week_date\": \"2018-01-06\",\n" + - " \"lowest_52_week_price\": 4175000,\n" + - " \"lowest_52_week_date\": \"2017-09-25\",\n" + - " \"timestamp\": 1524047026072\n" + - " }"; ResponseBody responseBody = ResponseBody.create(json, MediaType.get("application/json")); Request mockrequest = new Request.Builder().url("http://localhost").build(); Response mockresponse = new Response.Builder() .request(mockrequest) .protocol(Protocol.HTTP_1_1) - .code(200) + .code(400) .message("OK") .body(responseBody) .build(); @@ -81,4 +81,41 @@ void getOpeningPrice() throws IOException { assertTrue(response.contains("opening_price")); assertEquals(json, response); } + @DisplayName("ticker가 잘못된 요청이 들어갔을 때 log를 띄우는 지") + @Test + void callFailbyWrongTicker() throws IOException { + //given + + ResponseBody responseBody = ResponseBody.create(failJson, MediaType.get("application/json")); + Request mockrequest = new Request.Builder().url("http://localhost").build(); + Response mockresponse = new Response.Builder() + .request(mockrequest) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(responseBody) + .build(); + + when(client.newCall(any())).thenReturn(call); + when(call.execute()).thenReturn(mockresponse); + + //when + String response = bithumbAPIClient.getOpeningPrice(ticker); + + //then + assertTrue(response.contains("{}")); + } + + //무응답도 대응필요함 + @DisplayName("실행시 API의 response가 실패할 경우 에러를 던지는 지") + @Test + void callOpeningPriceFails() throws IOException { + //given + //when + when(client.newCall(any())).thenReturn(call); + when(call.execute()).thenThrow(new IOException("API 요청 중 예외 발생")); + + //then + assertThrows(RuntimeException.class, () -> bithumbAPIClient.getOpeningPrice(ticker)); + } } \ No newline at end of file From 8376686cd5387ece9cfee1fef465db8921b037a6 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Fri, 30 May 2025 17:46:26 +0900 Subject: [PATCH 09/31] =?UTF-8?q?config:=20=ED=98=B8=EA=B0=80=20=EC=88=98?= =?UTF-8?q?=EC=A7=91=20=EC=8B=9C=EA=B0=81=20=EC=88=98=EC=A0=95=20-=20?= =?UTF-8?q?=EB=A7=A4=EC=9D=BC=20=EC=9E=90=EC=A0=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=AC=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 41878e3a..7d298092 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,5 +46,5 @@ server: bot-handler: fixed-rate: 5000 # 5초마다 실행 - corn : "30 * * * * *" # 매일 자정마다 호가 + corn : "0 0 0 * * *" # 매일 자정마다 호가 order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 From 4d87da46fa26adf9ceeba3ed55f979e1efc42603 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Fri, 30 May 2025 17:47:02 +0900 Subject: [PATCH 10/31] =?UTF-8?q?test:=20=ED=98=B8=EA=B0=80=20=EC=88=98?= =?UTF-8?q?=EC=A7=91=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../realitybot/api/RefresherRunnerTest.java | 26 +++++++++++++++++++ .../api/UnitPriceRefresherTest.java | 9 +++---- .../coin/realitybot/dto/OpeningPriceTest.java | 2 +- .../realitybot/vo/UnitPricePolicyTest.java | 4 +-- 4 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java new file mode 100644 index 00000000..be5b4ce5 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java @@ -0,0 +1,26 @@ +package com.cleanengine.coin.realitybot.api; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@SpringBootTest +public class RefresherRunnerTest { + @SpyBean + private UnitPriceRefresher unitPriceRefresher; + + @DisplayName("어플리케이션 실행 시 호가 단위 수집") + @Test + public void runwithrefrecher(){ + verify(unitPriceRefresher,times(1)).run(any(ApplicationArguments.class)); + verify(unitPriceRefresher,times(1)).initializeUnitPrices(); + } + +// @DisplayName(" ") +} diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java index 3111096f..f0aa2532 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java @@ -11,17 +11,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class UnitPriceRefresherTest { +public class UnitPriceRefresherTest { @InjectMocks private UnitPriceRefresher unitPriceRefresher; @@ -37,7 +34,7 @@ class UnitPriceRefresherTest { @Mock private UnitPricePolicy unitPricePolicy; - @DisplayName("run 시작시 ") + @DisplayName("run 시작시 호가 단위를 불러오는 지 여부") @Test void testRefresherUnitPrice() { //given diff --git a/src/test/java/com/cleanengine/coin/realitybot/dto/OpeningPriceTest.java b/src/test/java/com/cleanengine/coin/realitybot/dto/OpeningPriceTest.java index 5073bfa7..7731ac34 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/dto/OpeningPriceTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/dto/OpeningPriceTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -class OpeningPriceTest { +public class OpeningPriceTest { OpeningPrice openingPrice = new OpeningPrice(); diff --git a/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java b/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java index 4d434a71..7debc977 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/vo/UnitPricePolicyTest.java @@ -5,10 +5,8 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class UnitPricePolicyTest { +public class UnitPricePolicyTest { private UnitPricePolicy unitPricePolicy; From 2d60130464b2c6208d0fe12338da7f239dd6ab94 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Sun, 1 Jun 2025 21:19:52 +0900 Subject: [PATCH 11/31] =?UTF-8?q?remove:=20=EB=AA=A8=EB=93=88=20=EA=B2=B0?= =?UTF-8?q?=ED=95=A9=20=EC=A0=84=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/dto/TestOrder.java | 27 ---- .../service/OrderQueueManagerService.java | 46 ------- .../service/VirtualTradeService.java | 118 ------------------ 3 files changed, 191 deletions(-) delete mode 100644 src/main/java/com/cleanengine/coin/realitybot/dto/TestOrder.java delete mode 100644 src/main/java/com/cleanengine/coin/realitybot/service/OrderQueueManagerService.java delete mode 100644 src/main/java/com/cleanengine/coin/realitybot/service/VirtualTradeService.java diff --git a/src/main/java/com/cleanengine/coin/realitybot/dto/TestOrder.java b/src/main/java/com/cleanengine/coin/realitybot/dto/TestOrder.java deleted file mode 100644 index 3c1bda4a..00000000 --- a/src/main/java/com/cleanengine/coin/realitybot/dto/TestOrder.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.cleanengine.coin.realitybot.dto; - -import lombok.*; - -@AllArgsConstructor -@Data -@Builder -@Getter -@Setter -public class TestOrder { - public enum Type{BUY,SELL} - private final Type type; - private double price; - private double volume; - private long timestamp; - - - @Override - public String toString() { - return "TestOrder{" + - "type=" + type + - ", price=" + price + - ", volum=" + volume + - ", timestamp=" + timestamp + - '}'; - } -} diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/OrderQueueManagerService.java b/src/main/java/com/cleanengine/coin/realitybot/service/OrderQueueManagerService.java deleted file mode 100644 index f737db01..00000000 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderQueueManagerService.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.cleanengine.coin.realitybot.service; - -import com.cleanengine.coin.realitybot.dto.TestOrder; -import lombok.Getter; - -import java.util.Comparator; -import java.util.PriorityQueue; - -@Getter -//@Component -public class OrderQueueManagerService { - /*해당 코드는 초기 개별 모듈로 작업할 때 가상의 체결을 만드는 코드였습니다. - * 이젠 쓰이지 않는 코드이나 어떤 에러가 발생할 때 재사용하기 위한 용도로 삭제하지 않았습니다. - * */ - - //체결용 출력 - private final PriorityQueue buyqueue = new PriorityQueue<>(new Comparator() { - @Override - public int compare(TestOrder o1, TestOrder o2) { - return Double.compare(o2.getPrice(), o1.getPrice());//가격이 높은 순 - } - }); - private final PriorityQueue sellqueue = new PriorityQueue<>(new Comparator() { - @Override - public int compare(TestOrder o1, TestOrder o2) { - return Double.compare(o1.getPrice(), o2.getPrice());//가격이 낮은 순 - } - }); - - //generator로부터 입력 - public void addBuyOrder(double price, double volume){ - buyqueue.offer(new TestOrder(TestOrder.Type.BUY,price,volume,System.currentTimeMillis())); - } - public void addSellOrder(double price, double volume){ - sellqueue.offer(new TestOrder(TestOrder.Type.SELL,price,volume,System.currentTimeMillis())); - } - - //큐 로그 확인용 - public void logAllOrders(){ - System.out.println("== BUY QUEUE =="); - buyqueue.forEach(System.out::println); - System.out.println("== SELL QUEUE =="); - sellqueue.forEach(System.out::println); - } - -} diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/VirtualTradeService.java b/src/main/java/com/cleanengine/coin/realitybot/service/VirtualTradeService.java deleted file mode 100644 index a32983d5..00000000 --- a/src/main/java/com/cleanengine/coin/realitybot/service/VirtualTradeService.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.cleanengine.coin.realitybot.service; - -import com.cleanengine.coin.realitybot.dto.TestOrder; - -import java.util.*; - -//@Service -public class VirtualTradeService { - private final OrderQueueManagerService queueManager; - private final PlatformVWAPService platformVWAPService; - - public VirtualTradeService(OrderQueueManagerService queueManager, PlatformVWAPService platformVWAPService) { - this.platformVWAPService = platformVWAPService; - this.queueManager = queueManager; - } - /*해당 코드는 초기 개별 모듈로 작업할 때 가상의 체결을 만드는 코드였습니다. - * 이젠 쓰이지 않는 코드이나 어떤 에러가 발생할 때 재사용하기 위한 용도로 삭제하지 않았습니다. - * */ - - //가상 주문 매칭 및 체결 처리를 담당하는 서비스 - public void matchOrder(){ - //매수, 매도 주문 큐 관리 - PriorityQueue buyQueue = queueManager.getBuyqueue(); - PriorityQueue sellQueue = queueManager.getSellqueue(); - - while(!buyQueue.isEmpty() && !sellQueue.isEmpty()){ - //주문 추출 - TestOrder buyOrder = buyQueue.peek(); //가장 높은 매수 주문 - TestOrder sellOrder = sellQueue.peek(); // 가장 낮은 매도 주문 - - //체결 조건 부여 : 현재 느슨한 체결 (1:1은 문제 발생/어짜피 매서드 호출 힘너무 쓰면 안됨) - //매수 희망가 >= 매도 희망가 - if ((long)buyOrder.getPrice() == (long)sellOrder.getPrice()){ //매도벽이 크게 세워짐.. - - //체결 가격을 중간값으로 설정 -// double matchedPrice = (buyOrder.getPrice() + sellOrder.getPrice())/2; // 느슨한 체결 조건 쓰니깐 문제 발생 - - //현재 매도가 기준 - double matchedPrice = sellOrder.getPrice(); - double matchedVolume = Math.min(buyOrder.getVolume(), sellOrder.getVolume()); //적은쪽으로 물량 설정 -// System.out.println("=== 체결 진행 - 가격 :"+matchedPrice+", 수량 : "+matchedVolume); - - //잔량 처리 - buyOrder.setVolume(buyOrder.getVolume() - matchedVolume); - sellOrder.setVolume(sellOrder.getVolume() - matchedVolume); - - //잔량 0 이하 주문 제거 - if (buyOrder.getVolume() <= 0) buyQueue.poll(); - if (sellOrder.getVolume() <= 0) sellQueue.poll(); - -// platformVWAPService.recordTrade(matchedPrice,matchedVolume); - } - else { - break; - } - - } - } - public void matchOrderbyIterator(){ - //queue 를 list로 변환 - List buyOrders = new ArrayList<>(queueManager.getBuyqueue()); - List sellOrders = new ArrayList<>(queueManager.getSellqueue()); - - // - buyOrders.sort(Comparator.comparing(TestOrder::getPrice).reversed()); - sellOrders.sort(Comparator.comparing(TestOrder::getPrice)); - - List excutedBuy = new ArrayList<>(); - List excutedSell = new ArrayList<>(); - - for (TestOrder buyOrder : buyOrders){ - for (TestOrder sellOrder : sellOrders){ - if ((int)buyOrder.getPrice() >= (int)sellOrder.getPrice()){ - double matchVolume = Math.min(buyOrder.getVolume(), sellOrder.getVolume()); - if (matchVolume <=0) continue; - buyOrder.setVolume(buyOrder.getVolume() - matchVolume); - sellOrder.setVolume(sellOrder.getVolume() - matchVolume); - - if (buyOrder.getVolume() <= 0){ - excutedBuy.add(buyOrder); - break; - } - if (sellOrder.getVolume() <= 0){ - excutedSell.add(sellOrder); -// break; - } - } - - } - } - queueManager.getBuyqueue().removeAll(excutedBuy); - queueManager.getSellqueue().removeAll(excutedSell); - - } - - //매서드 종료 시 호가창 요약 - private void printSummary(PriorityQueue queue, Comparator sortOrder) { - Map summary = new TreeMap<>(sortOrder); - - for (TestOrder order : queue) { - int price = (int) order.getPrice(); // 호가 기준 - double volume = order.getVolume(); - summary.put(price, summary.getOrDefault(price, 0.0) + volume); - } - - for (Map.Entry entry : summary.entrySet()) { - System.out.printf("호가 %d원 : %.4f 개%n", entry.getKey(), entry.getValue()); - } - } - - //전체 호가창 콘솔 출력 - public void printOrderSummary() { - System.out.println("=== SELL ORDER SUMMARY ==="); - printSummary(queueManager.getSellqueue(), Comparator.reverseOrder()); // ⬇ 고가 → 저가 - System.out.println("=== BUY ORDER SUMMARY ==="); - printSummary(queueManager.getBuyqueue(), Comparator.reverseOrder()); // ⬆ 저가 → 고가 - } -} From 6879f73d54978a2306aefb6a6af1dd5866446c5b Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Sun, 1 Jun 2025 21:22:52 +0900 Subject: [PATCH 12/31] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/ApiScheduler.java | 2 +- .../coin/realitybot/api/BithumbAPIClient.java | 5 ++- .../realitybot/controller/ApiController.java | 23 +++++------ .../coin/realitybot/parser/TickParser.java | 4 +- .../coin/realitybot/vo/VWAPState.java | 40 +++---------------- 5 files changed, 23 insertions(+), 51 deletions(-) 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 a586075d..5f6f1e62 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -43,7 +43,7 @@ public void MarketAllRequest() throws InterruptedException { public void MarketDataRequest(String ticker){ this.ticker = ticker; String rawJson = bithumbAPIClient.get(ticker); //api raw데이터 - List gson = TickParser.parseGson(rawJson); //json을 list로 변환 + List gson = tickParser.parseGson(rawJson); //json을 list로 변환 ApiVWAPService apiVWAPService = tickServiceManager.getService(ticker); long lastSeqId = lastSequentialIdMap.getOrDefault(ticker,0L); diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index 727680b4..5670a273 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -28,6 +28,9 @@ public String get(String ticker){ //API를 responseBody에 담아 반환 .addHeader("accept", "application/json") .build(); try (Response response = client.newCall(request).execute()){ + if ((response.code() == 400)){ + log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker); + } String responseBody = response.body().string(); // return gson.toJson(response.body().string()); log.debug("{}의 Bithumb API 응답 : {}",ticker,responseBody); @@ -44,7 +47,7 @@ public String getOpeningPrice(String ticker){ .addHeader("accept", "application/json") .build(); try (Response response = client.newCall(request).execute()){ - if ((response.code() == 200)){ + if ((response.code() == 400)){ log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker); } String responseBody = response.body().string(); diff --git a/src/main/java/com/cleanengine/coin/realitybot/controller/ApiController.java b/src/main/java/com/cleanengine/coin/realitybot/controller/ApiController.java index 6de9a720..2aa87c81 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/controller/ApiController.java +++ b/src/main/java/com/cleanengine/coin/realitybot/controller/ApiController.java @@ -1,20 +1,19 @@ package com.cleanengine.coin.realitybot.controller; -import com.cleanengine.coin.realitybot.api.BithumbAPIClient; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") public class ApiController { - private final BithumbAPIClient bithumbAPIClient; - public ApiController(BithumbAPIClient bithumbAPIClient) { - this.bithumbAPIClient = bithumbAPIClient; - } - @GetMapping("/test") - public String getApiData(){ -// return bithumbAPIClient.get(ticekr); - return null; - - }} + //초기 api 작동 확인용 -> realitybot controller로 전환 필요 +// private final BithumbAPIClient bithumbAPIClient; +// public ApiController(BithumbAPIClient bithumbAPIClient) { +// this.bithumbAPIClient = bithumbAPIClient; +// } +// @GetMapping("/test/{tickerName}") +// public String getApiData(@PathVariable String tickerName) { +// System.out.println("tickername 출력"+tickerName); +// return bithumbAPIClient.get(tickerName); +// } +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java b/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java index 0e346718..be643cc8 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java +++ b/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java @@ -15,9 +15,9 @@ @Slf4j @Getter public class TickParser { - private static final Gson gson = new Gson(); + private final Gson gson = new Gson(); - public static List parseGson(String json) { + public List parseGson(String json) { return gson.fromJson(json, new TypeToken>() {}.getType()); } /* diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java b/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java index 8f7e6e7f..ae5064f5 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java @@ -3,13 +3,13 @@ import com.cleanengine.coin.trade.entity.Trade; import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; -import java.util.LinkedList; import java.util.List; -import java.util.Queue; @Getter @Setter +@Slf4j public class VWAPState { public VWAPState(String ticker) { @@ -17,8 +17,8 @@ public VWAPState(String ticker) { } private String ticker; - private final Queue tradeQueue = new LinkedList<>(); //테스트를 위한 큐 -> 체결 db에서 데이터 조회 - private int maxQueueSize = 10; +// private final Queue tradeQueue = new LinkedList<>(); //테스트를 위한 큐 -> 체결 db에서 데이터 조회 +// private int maxQueueSize = 10; private double totalPriceVolume = 0; private double totalVolume = 0; @@ -27,15 +27,7 @@ public VWAPState(String ticker) { //이건 처음에나 필요했지 queue나 10개씩 받아오면서 필요 없는 로직이 되어버림 public void recordTrade(double price, double volume) { - -// if (volume <= 0) return; -// if (tradeQueue.size() >= maxQueueSize) { -// Vwap removed = tradeQueue.poll(); -// totalPriceVolume -= removed.price * removed.volume; -// totalVolume -= removed.volume; -// } - - tradeQueue.offer(new Vwap(price, volume)); +// tradeQueue.offer(new Vwap(price, volume)); //오로지 계산에만 목적을 둠 totalPriceVolume += price * volume; totalVolume += volume; } @@ -50,29 +42,7 @@ public void calculateVWAPbyTrades(List trades) { double price = trade.getPrice(); double volume = trade.getSize(); recordTrade(price,volume); -// if (volume <= 0) continue; -// totalPriceVolume += price * volume; -// totalVolume += volume; - } getVWAP(); } - - private static class Vwap { //원래 trade였는데 가상 계산 떄문에 냅두기 - double price; - double volume; - - public Vwap(double price, double volume) { - this.price = price; - this.volume = volume; - } - - @Override - public String toString() { - return "Trade{" + - "price=" + price + - ", volume=" + volume + - '}'; - } - } } From 142717c98e2f9816c01f6d10f24566876b138d28 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Sun, 1 Jun 2025 21:23:08 +0900 Subject: [PATCH 13/31] =?UTF-8?q?test:=20=EB=8B=A8=EC=9C=84=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../realitybot/RealitybotCoreTestSuite.java | 4 +- .../coin/realitybot/api/ApiSchedulerTest.java | 82 ++++++++++++ .../coin/realitybot/vo/VWAPStateTest.java | 120 ++++++++++++++++++ 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/vo/VWAPStateTest.java diff --git a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java index dcc08e85..c17b9b2b 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java +++ b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java @@ -5,6 +5,7 @@ import com.cleanengine.coin.realitybot.api.UnitPriceRefresherTest; import com.cleanengine.coin.realitybot.dto.OpeningPriceTest; import com.cleanengine.coin.realitybot.vo.UnitPricePolicyTest; +import com.cleanengine.coin.realitybot.vo.VWAPStateTest; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @@ -14,7 +15,8 @@ BithumbAPIClientTest.class, UnitPriceRefresherTest.class, OpeningPriceTest.class, - UnitPricePolicyTest.class + VWAPStateTest.class, + UnitPricePolicyTest.class, }) public class RealitybotCoreTestSuite { } diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java new file mode 100644 index 00000000..e2174073 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java @@ -0,0 +1,82 @@ +package com.cleanengine.coin.realitybot.api; + +import com.cleanengine.coin.order.domain.Asset; +import com.cleanengine.coin.order.infra.AssetRepository; +import com.cleanengine.coin.realitybot.dto.Ticks; +import com.cleanengine.coin.realitybot.parser.TickParser; +import com.cleanengine.coin.realitybot.service.ApiVWAPService; +import com.cleanengine.coin.realitybot.service.OrderGenerateService; +import com.cleanengine.coin.realitybot.service.TickServiceManager; +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.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ApiSchedulerTest { + + @InjectMocks + private ApiScheduler apiScheduler; + + @Mock + private BithumbAPIClient apiClient; + @Mock + private TickParser tickParser; + @Mock + private OrderGenerateService orderGenerateService; + @Mock + ApiVWAPService apiVWAPService; + @Mock + private TickServiceManager tickServiceManager; + @Mock + private Map lastSequentialIdMap = new ConcurrentHashMap<>(); + @Mock + private AssetRepository assetRepository; + + + + @Test + void marketAllRequestCallsAllTickers() throws InterruptedException { + List assets = List.of( + new Asset("BTC", "비트코인", null), + new Asset("TRUMP", "트럼프", null), + new Asset("ETH", "이더리움", null), + new Asset("DOGE", "도지코인", null), + new Asset("USDT", "테더", null), + new Asset("PEPE", "페페", null), + new Asset("XRP", "리플", null), + new Asset("SOL", "솔라나", null), + new Asset("SUI", "수이", null), + 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 + when(assetRepository.findAll()).thenReturn(assets); + when(apiClient.get(anyString())).thenReturn("[{data:...}]"); + when(tickParser.parseGson(anyString())).thenReturn(testTicks); + when(tickServiceManager.getService(anyString())).thenReturn(apiVWAPService); + doNothing().when(apiVWAPService).addTick(any()); + System.out.println(assets.size()); + //when + apiScheduler.MarketAllRequest(); + + //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/vo/VWAPStateTest.java b/src/test/java/com/cleanengine/coin/realitybot/vo/VWAPStateTest.java new file mode 100644 index 00000000..76a57577 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/vo/VWAPStateTest.java @@ -0,0 +1,120 @@ +package com.cleanengine.coin.realitybot.vo; + +import com.cleanengine.coin.trade.entity.Trade; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class VWAPStateTest { + + + @Test + void testVWAPwithSingleTrade() { + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + vwapState.recordTrade(18000.0,10.0); + assertEquals(vwapState.getTotalPriceVolume(), 180000.0); + assertEquals(vwapState.getTotalVolume(), 10.0); + assertEquals(vwapState.getVWAP(), 18000.0); + } + @Test + void testVWAPwith0VolumeTrade() { + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + vwapState.recordTrade(18000.0,0); + assertEquals(vwapState.getTotalPriceVolume(), 0); + assertEquals(vwapState.getTotalVolume(), 0); + assertEquals(vwapState.getVWAP(), 0); + } + @Test + void testVWAPwith0PriceTrade() { + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + vwapState.recordTrade(0,100); + assertEquals(vwapState.getTotalPriceVolume(), 0); + assertEquals(vwapState.getTotalVolume(), 100); + assertEquals(vwapState.getVWAP(), 0); + } + @Test + void testVWAPStackTrades(){ + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + vwapState.recordTrade(18000,100); + vwapState.recordTrade(17000,70); + vwapState.recordTrade(16000,50); + vwapState.recordTrade(15000,30); + vwapState.recordTrade(14000,10); + assertEquals(vwapState.getTotalPriceVolume(), 4380000.0); + assertEquals(vwapState.getTotalVolume(), 260.0); + assertEquals(vwapState.getVWAP(), 16846.1538,0.0001); + } + @Test + void testVWAPStackTradesWith0Volumes(){ + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + vwapState.recordTrade(18000,0); + vwapState.recordTrade(17000,0); + vwapState.recordTrade(16000,0); + vwapState.recordTrade(15000,0); + vwapState.recordTrade(14000,0); + assertEquals(vwapState.getTotalPriceVolume(), 0); + assertEquals(vwapState.getTotalVolume(), 0); + assertEquals(vwapState.getVWAP(), 0.0); + } + @Test + void testVWAPStack1Trades(){ + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + vwapState.recordTrade(18000,0); + vwapState.recordTrade(17000,0); + vwapState.recordTrade(16000,0); + vwapState.recordTrade(15000,0); + vwapState.recordTrade(14000,1); + assertEquals(vwapState.getTotalPriceVolume(), 14000.0); + assertEquals(vwapState.getTotalVolume(), 1); + assertEquals(vwapState.getVWAP(), 14000.0); + } +/* @DisplayName("10개 이상의 거래를 보낼 경우 최신 10개만 계산하는 지") + @Test + void testVWAPStacksOver10Trades(){ + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + for (int i = 1; i < 16; i++) { + double price = i*100; + System.out.println(price); + vwapState.recordTrade(price,1); + } + System.out.println(vwapState.getTotalPriceVolume()); + System.out.println(vwapState.getTotalVolume()); + System.out.println(vwapState.getVWAP()); + assertEquals(vwapState.getVWAP(), 0); + }*/ + @Test + void getVWAP() { + } + + @Test + void TestcalculateVWAPbyTrades() { + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + List trades = List.of( + new Trade(1, "BTC", LocalDateTime.now(), 2, 1, 10000.0, 10.0), // 100000 + new Trade(2, "BTC", LocalDateTime.now(), 2, 1, 11000.0, 10.0), // 110000 + new Trade(3, "BTC", LocalDateTime.now(), 2, 1, 12000.0, 10.0), // 120000 + new Trade(4, "BTC", LocalDateTime.now(), 2, 1, 13000.0, 10.0), // 130000 + new Trade(5, "BTC", LocalDateTime.now(), 2, 1, 14000.0, 10.0), // 140000 + new Trade(6, "BTC", LocalDateTime.now(), 2, 1, 15000.0, 10.0), // 150000 + new Trade(7, "BTC", LocalDateTime.now(), 2, 1, 16000.0, 10.0), // 160000 + new Trade(8, "BTC", LocalDateTime.now(), 2, 1, 17000.0, 10.0), // 170000 + new Trade(9, "BTC", LocalDateTime.now(), 2, 1, 18000.0, 10.0), // 180000 + new Trade(10, "BTC", LocalDateTime.now(), 2, 1, 19000.0, 10.0), // 190000 + new Trade(11, "BTC", LocalDateTime.now(), 2, 1, 20000.0, 10.0) // 200000 + ); + vwapState.calculateVWAPbyTrades(trades); + System.out.println(vwapState.getVWAP()); + assertEquals(vwapState.getVWAP(),15000.0); + } +} \ No newline at end of file From 6ce796671f8162f84db8a679492ca056d3f0321c Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Mon, 2 Jun 2025 05:19:17 +0900 Subject: [PATCH 14/31] =?UTF-8?q?test:=20=EC=8B=9C=EC=84=B8=20=EC=88=98?= =?UTF-8?q?=EC=A7=91=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/BithumbAPIClient.java | 2 +- .../realitybot/config/ApiClientConfig.java | 5 -- .../realitybot/RealitybotCoreTestSuite.java | 14 ++- .../coin/realitybot/api/ApiSchedulerTest.java | 2 +- .../realitybot/api/BithumbAPIClientTest.java | 54 +++++++++++- .../config/ApiClientConfigTest.java | 26 ++++++ .../config/SchedulerConfigTest.java | 85 +++++++++++++++++++ .../service/PlatformVWAPServiceTest.java | 78 +++++++++++++++++ .../service/TickServiceManagerTest.java | 47 ++++++++++ 9 files changed, 300 insertions(+), 13 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/realitybot/config/ApiClientConfigTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/config/SchedulerConfigTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/service/TickServiceManagerTest.java diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index 5670a273..207a6dc3 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -14,7 +14,7 @@ @RequiredArgsConstructor @Slf4j public class BithumbAPIClient { - private OkHttpClient client = new OkHttpClient(); + private final OkHttpClient client; private String ticker; diff --git a/src/main/java/com/cleanengine/coin/realitybot/config/ApiClientConfig.java b/src/main/java/com/cleanengine/coin/realitybot/config/ApiClientConfig.java index fcbae119..8574944a 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/config/ApiClientConfig.java +++ b/src/main/java/com/cleanengine/coin/realitybot/config/ApiClientConfig.java @@ -16,9 +16,4 @@ public OkHttpClient okHttpClient() { // .addInterceptor() .build(); } - - @Bean - public Queue ticksQueue(){ //공통화 시킴 - return new LinkedList<>(); - } } diff --git a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java index c17b9b2b..cded1567 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java +++ b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java @@ -1,9 +1,12 @@ package com.cleanengine.coin.realitybot; -import com.cleanengine.coin.realitybot.api.BithumbAPIClientTest; -import com.cleanengine.coin.realitybot.api.RefresherRunnerTest; -import com.cleanengine.coin.realitybot.api.UnitPriceRefresherTest; +import com.cleanengine.coin.realitybot.api.*; +import com.cleanengine.coin.realitybot.config.ApiClientConfigTest; +import com.cleanengine.coin.realitybot.config.SchedulerConfigTest; import com.cleanengine.coin.realitybot.dto.OpeningPriceTest; +import com.cleanengine.coin.realitybot.service.PlatformVWAPServiceTest; +import com.cleanengine.coin.realitybot.service.TickServiceManager; +import com.cleanengine.coin.realitybot.service.TickServiceManagerTest; import com.cleanengine.coin.realitybot.vo.UnitPricePolicyTest; import com.cleanengine.coin.realitybot.vo.VWAPStateTest; import org.junit.platform.suite.api.SelectClasses; @@ -17,6 +20,11 @@ OpeningPriceTest.class, VWAPStateTest.class, UnitPricePolicyTest.class, + SchedulerConfigTest.class, + ApiSchedulerTest.class, + ApiClientConfigTest.class, + PlatformVWAPServiceTest.class, + TickServiceManagerTest.class }) public class RealitybotCoreTestSuite { } 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 e2174073..946a847b 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java @@ -20,7 +20,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class ApiSchedulerTest { +public class ApiSchedulerTest { @InjectMocks private ApiScheduler apiScheduler; diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java index eb9ba170..e03b2b60 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java @@ -27,7 +27,18 @@ public class BithumbAPIClientTest { void get() { } private String ticker = "BTC"; - private String json = "{\n" + + private String tradeJson = " {\n" + + " \"market\": \"KRW-BTC\",\n" + + " \"trade_date_utc\": \"2018-04-18\",\n" + + " \"trade_time_utc\": \"10:19:58\",\n" + + " \"timestamp\": 1524046798000,\n" + + " \"trade_price\": 8616000,\n" + + " \"trade_volume\": 0.03060688,\n" + + " \"prev_closing_price\": 8450000,\n" + + " \"chane_price\": 166000,\n" + + " \"ask_bid\": \"ASK\"\n" + + " }"; + private String openingJson = "{\n" + " \"market\": \"KRW-BTC\",\n" + " \"trade_date\": \"20180418\",\n" + " \"trade_time\": \"102340\",\n" + @@ -57,11 +68,36 @@ void get() { " }"; private String failJson = "{}"; + @DisplayName("실행시 API의 response에 trade 값이 들어오는 지") + @Test + void callTradePrice() throws IOException { + //given + ResponseBody responseBody = ResponseBody.create(tradeJson, MediaType.get("application/json")); + Request mockrequest = new Request.Builder().url("http://localhost").build(); + Response mockresponse = new Response.Builder() + .request(mockrequest) + .protocol(Protocol.HTTP_1_1) + .code(400) + .message("OK") + .body(responseBody) + .build(); + + when(client.newCall(any())).thenReturn(call); + when(call.execute()).thenReturn(mockresponse); + + //when + String response = bithumbAPIClient.get(ticker); + + //then + assertTrue(response.contains("trade_price")); + assertEquals(tradeJson, response); + } + @DisplayName("실행시 API의 response에 Opening_price 값이 들어오는 지") @Test void callOpeningPrice() throws IOException { //given - ResponseBody responseBody = ResponseBody.create(json, MediaType.get("application/json")); + ResponseBody responseBody = ResponseBody.create(openingJson, MediaType.get("application/json")); Request mockrequest = new Request.Builder().url("http://localhost").build(); Response mockresponse = new Response.Builder() .request(mockrequest) @@ -79,7 +115,7 @@ void callOpeningPrice() throws IOException { //then assertTrue(response.contains("opening_price")); - assertEquals(json, response); + assertEquals(openingJson, response); } @DisplayName("ticker가 잘못된 요청이 들어갔을 때 log를 띄우는 지") @Test @@ -115,6 +151,18 @@ void callOpeningPriceFails() throws IOException { when(client.newCall(any())).thenReturn(call); when(call.execute()).thenThrow(new IOException("API 요청 중 예외 발생")); + //then + assertThrows(RuntimeException.class, () -> bithumbAPIClient.getOpeningPrice(ticker)); + } + //무응답도 대응필요함 + @DisplayName("실행시 API의 response가 실패할 경우 에러를 던지는 지") + @Test + void callTradePriceFails() throws IOException { + //given + //when + when(client.newCall(any())).thenReturn(call); + when(call.execute()).thenThrow(new IOException("API 요청 중 예외 발생")); + //then assertThrows(RuntimeException.class, () -> bithumbAPIClient.getOpeningPrice(ticker)); } diff --git a/src/test/java/com/cleanengine/coin/realitybot/config/ApiClientConfigTest.java b/src/test/java/com/cleanengine/coin/realitybot/config/ApiClientConfigTest.java new file mode 100644 index 00000000..79f5ab21 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/config/ApiClientConfigTest.java @@ -0,0 +1,26 @@ +package com.cleanengine.coin.realitybot.config; + +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ApiClientConfig.class) +public class ApiClientConfigTest { + + @Autowired + OkHttpClient okHttpClient; + + @DisplayName("Bean 정상 등록 여부") + @Test + void CreateokHttpClientBean() { + assertNotNull(okHttpClient); + } + +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/config/SchedulerConfigTest.java b/src/test/java/com/cleanengine/coin/realitybot/config/SchedulerConfigTest.java new file mode 100644 index 00000000..59b4fd78 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/config/SchedulerConfigTest.java @@ -0,0 +1,85 @@ +package com.cleanengine.coin.realitybot.config; + +import com.cleanengine.coin.realitybot.api.ApiScheduler; +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 org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.config.Task; + +import java.lang.reflect.Field; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class SchedulerConfigTest { + + @Mock + ApiScheduler apiScheduler; + + @Mock + ScheduledTaskRegistrar scheduledTaskRegistrar; + + @InjectMocks + SchedulerConfig schedulerConfig; + + @BeforeEach + void setUp() throws NoSuchFieldException, IllegalAccessException { + Field fixedRateField = SchedulerConfig.class.getDeclaredField("fixedRate"); + fixedRateField.setAccessible(true); + fixedRateField.set(schedulerConfig, Duration.ofSeconds(1)); + } + + + @DisplayName("fixedrate를 적용 후 정상 작동하는 지") + @Test + void testConfigureTasksOnFixedRate() throws NoSuchFieldException, IllegalAccessException, InterruptedException { + //when + schedulerConfig.configureTasks(scheduledTaskRegistrar); + + //then + ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(Runnable.class);//스케줄러에 등록된 작업 + ArgumentCaptor intervalCaptor = ArgumentCaptor.forClass(Duration.class);//실행 주기 + //mock에게 전달 된 인자를 캡처해서 확인가능하게 해줌 + //내부 속성을 확인할 때, 동적으로 생성된 값을 검증 할 때 + + verify(scheduledTaskRegistrar).addFixedRateTask(taskCaptor.capture(), intervalCaptor.capture()); + //addFixedRateTask를 실행 시 인자 두개를 캡쳐함 + + Runnable task = taskCaptor.getValue(); + //그 캡처된 인자중 task는 실제 실행하는 작업 (schedulerconfig에 구현한 것 -> apiScheduler.MarketAllRequest();) + task.run(); //가져와서 동적으로 실행할 수 있게 됨 -> apiScheduler.MarketAllRequest(); + + verify(apiScheduler).MarketAllRequest(); //작동 검증 + + Duration interval = intervalCaptor.getValue(); + assertEquals(Duration.ofSeconds(1), interval); + } + @DisplayName("marketallrequest가 예외 발생 시 에러를 던지는 지 확인") + @Test + void testCheckErrorbyMarketallRequest() throws InterruptedException { + //given + doThrow(new InterruptedException()).when(apiScheduler).MarketAllRequest(); + //메서드 실행 시 에러 던지도록 셋팅 + + //when + schedulerConfig.configureTasks(scheduledTaskRegistrar); + + //then + ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(Runnable.class);//스케줄러에 등록된 작업 + verify(scheduledTaskRegistrar).addFixedRateTask(taskCaptor.capture(), any(Duration.class)); + + Runnable task = taskCaptor.getValue(); + assertThrows(RuntimeException.class, () -> task.run()); + } + +} \ 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 new file mode 100644 index 00000000..ad262f4e --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java @@ -0,0 +1,78 @@ +package com.cleanengine.coin.realitybot.service; + +import com.cleanengine.coin.realitybot.vo.VWAPState; +import com.cleanengine.coin.trade.entity.Trade; +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 java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class PlatformVWAPServiceTest { + + @InjectMocks + private PlatformVWAPService platformVWAPService; + + @Mock + private VWAPState vwapState; + + @DisplayName("10개 이하일 때, APIVWAP 기준으로 랜덤값이 반환되는 지") + @Test + void testCalculateVWAPLessThan10Trades() { + //give + String ticker = "BTC"; + List trades = List.of( + new Trade(1, "BTC", LocalDateTime.now(), 2, 1, 10000.0, 10.0), // 100000 + new Trade(2, "BTC", LocalDateTime.now(), 2, 1, 11000.0, 10.0), // 110000 + new Trade(3, "BTC", LocalDateTime.now(), 2, 1, 12000.0, 10.0) // 120000 + );//이게 적용되면 10000원대 + double apiVWAP = 1000.0; //0.1%의 보정값 + + //when + double result = platformVWAPService.calculateVWAPbyTrades(ticker, trades, apiVWAP); + + //than + assertEquals(apiVWAP, result,1); + assertTrue(result>=999.0 && result<=1001.0); + } + + @DisplayName("10개 이상일 때, trades 기준으로 계산되는 지") + @Test + void testCalculateVWAPMoreThan10Trades() { + //given + String ticker = "BTC"; + List trades = List.of( + new Trade(1, "BTC", LocalDateTime.now(), 2, 1, 10000.0, 10.0), // 100000 + new Trade(2, "BTC", LocalDateTime.now(), 2, 1, 11000.0, 10.0), // 110000 + new Trade(3, "BTC", LocalDateTime.now(), 2, 1, 12000.0, 10.0), // 120000 + new Trade(4, "BTC", LocalDateTime.now(), 2, 1, 13000.0, 10.0), // 130000 + new Trade(5, "BTC", LocalDateTime.now(), 2, 1, 14000.0, 10.0), // 140000 + new Trade(6, "BTC", LocalDateTime.now(), 2, 1, 15000.0, 10.0), // 150000 + new Trade(7, "BTC", LocalDateTime.now(), 2, 1, 16000.0, 10.0), // 160000 + new Trade(8, "BTC", LocalDateTime.now(), 2, 1, 17000.0, 10.0), // 170000 + new Trade(9, "BTC", LocalDateTime.now(), 2, 1, 18000.0, 10.0), // 180000 + new Trade(10, "BTC", LocalDateTime.now(), 2, 1, 19000.0, 10.0), // 190000 + new Trade(11, "BTC", LocalDateTime.now(), 2, 1, 20000.0, 10.0) // 200000 + ); + double apiVWAP = 1000.0; + when(vwapState.getVWAP()).thenReturn(15000.0); + platformVWAPService.vwapMap.put(ticker,vwapState); + //when + double result = platformVWAPService.calculateVWAPbyTrades(ticker, trades, apiVWAP); + + //then + verify(vwapState).calculateVWAPbyTrades(trades); + verify(vwapState).getVWAP(); + assertEquals( 15000.0,result); + } + //todo generatevwap null확인안함 +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/TickServiceManagerTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/TickServiceManagerTest.java new file mode 100644 index 00000000..7ca9b9b8 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/service/TickServiceManagerTest.java @@ -0,0 +1,47 @@ +package com.cleanengine.coin.realitybot.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static org.junit.jupiter.api.Assertions.*; + +public class TickServiceManagerTest { +// @InjectMocks + private TickServiceManager tickServiceManager = new TickServiceManager(); + + @DisplayName("최초 ticker 입력시 not null이여야 함") + @Test + void getNewService() { + //given + String ticker = "BTC"; + //when + ApiVWAPService service = tickServiceManager.getService(ticker); + //then + assertNotNull(service); + } + @DisplayName("같은 ticker일 경우 동일 객체 반환") + @Test + void checksDuplication() { + //given + String ticker = "BTC"; + //when + ApiVWAPService service1 = tickServiceManager.getService(ticker); + ApiVWAPService service2 = tickServiceManager.getService(ticker); + //then + assertSame(service1, service2); + } + + @DisplayName("다른 ticker일 경우 다른 인스턴스 반환") + @Test + void checksOthers() { + //given + //when + ApiVWAPService service1 = tickServiceManager.getService("BTC"); + ApiVWAPService service2 = tickServiceManager.getService("TRUMP"); + //then + assertNotSame(service1, service2); + } + +} \ No newline at end of file From 7effaf4d32a42c26e6a69591103062227ad8be75 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Mon, 2 Jun 2025 17:48:55 +0900 Subject: [PATCH 15/31] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/ApiScheduler.java | 14 +++--- .../realitybot/config/SchedulerConfig.java | 11 +++-- .../coin/realitybot/domain/APIVWAPState.java | 43 +++++++++++++++++ .../realitybot/domain/PlatformVWAPState.java | 33 +++++++++++++ .../realitybot/domain/VWAPCalculator.java | 25 ++++++++++ .../realitybot/service/ApiVWAPService.java | 47 ------------------ .../service/PlatformVWAPService.java | 8 ++-- .../service/TickServiceManager.java | 7 +-- .../coin/realitybot/vo/VWAPState.java | 48 ------------------- 9 files changed, 123 insertions(+), 113 deletions(-) create mode 100644 src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/domain/PlatformVWAPState.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/domain/VWAPCalculator.java delete mode 100644 src/main/java/com/cleanengine/coin/realitybot/service/ApiVWAPService.java delete mode 100644 src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java 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 5f6f1e62..cf426b3c 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -4,9 +4,9 @@ import com.cleanengine.coin.order.domain.Asset; import com.cleanengine.coin.order.infra.AssetRepository; import com.cleanengine.coin.realitybot.dto.Ticks; -import com.cleanengine.coin.realitybot.service.ApiVWAPService; -import com.cleanengine.coin.realitybot.service.OrderGenerateService; import com.cleanengine.coin.realitybot.parser.TickParser; +import com.cleanengine.coin.realitybot.domain.APIVWAPState; +import com.cleanengine.coin.realitybot.service.OrderGenerateService; import com.cleanengine.coin.realitybot.service.TickServiceManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,7 +36,7 @@ public void MarketAllRequest() throws InterruptedException { for (Asset ticker : tickers){ String tickerName = ticker.getTicker(); MarketDataRequest(tickerName); - Thread.sleep(500); +// Thread.sleep(500); } } @@ -45,21 +45,21 @@ public void MarketDataRequest(String ticker){ String rawJson = bithumbAPIClient.get(ticker); //api raw데이터 List gson = tickParser.parseGson(rawJson); //json을 list로 변환 - ApiVWAPService apiVWAPService = tickServiceManager.getService(ticker); + APIVWAPState apiVWAPState = tickServiceManager.getService(ticker); long lastSeqId = lastSequentialIdMap.getOrDefault(ticker,0L); //api 중복검사하여 queue에 저장하기 for (int i = gson.size()-1; i >=0 ; i--) {//2차 : 10 - 역순으로 정렬되어 - 순회해야 함. Ticks ticks = gson.get(i); if (ticks.getSequential_id() > lastSeqId){ //중복 검증용 - apiVWAPService.addTick(ticks); + apiVWAPState.addTick(ticks); lastSeqId = Math.max(lastSeqId, ticks.getSequential_id()); //중복 id 갱신 } } lastSequentialIdMap.put(ticker,lastSeqId); - double vwap = apiVWAPService.getVWAP(); - double volume = apiVWAPService.getAvgVolumePerOrder(); + double vwap = apiVWAPState.getVWAP(); + double volume = apiVWAPState.getAvgVolumePerOrder(); orderGenerateService.generateOrder(ticker,vwap,volume); //1tick 당 매수/매도 3개씩 제작 // log.info("작동확인 {}의 가격 : {} , 볼륨 : {}",ticker, vwap, volume); } diff --git a/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java b/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java index 1dcef296..c26118a1 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java +++ b/src/main/java/com/cleanengine/coin/realitybot/config/SchedulerConfig.java @@ -1,7 +1,6 @@ package com.cleanengine.coin.realitybot.config; import com.cleanengine.coin.realitybot.api.ApiScheduler; -import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; @@ -12,16 +11,20 @@ @Configuration @EnableScheduling -@RequiredArgsConstructor +//@RequiredArgsConstructor public class SchedulerConfig implements SchedulingConfigurer { //멀티쓰레드 환경 x // @Autowired // private TaskScheduler apiScheduler; private final ApiScheduler apiScheduler; - @Value("${bot-handler.fixed-rate}") - private Duration fixedRate; + private final Duration fixedRate; + + protected SchedulerConfig(ApiScheduler apiScheduler, @Value("${bot-handler.fixed-rate}") Duration fixedRate) { + this.apiScheduler = apiScheduler; + this.fixedRate = fixedRate; + } @Override public void configureTasks(ScheduledTaskRegistrar registrar) { diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java b/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java new file mode 100644 index 00000000..fa22e48b --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java @@ -0,0 +1,43 @@ +package com.cleanengine.coin.realitybot.domain; + + +import com.cleanengine.coin.realitybot.dto.Ticks; +import lombok.Getter; + +import java.util.LinkedList; +import java.util.Queue; + +@Getter +public class APIVWAPState { + private final Queue ticksQueue = new LinkedList<>(); + private VWAPCalculator calculator = new VWAPCalculator(); + private int maxQueueSize = 10; + + public void addTick(Ticks tick){ + if (ticksQueue.size() >= maxQueueSize) { + //10개 이상이 되면 선착순으로 제거해나감 + Ticks removed = ticksQueue.poll(); + calculator.removeTrade(removed.getTrade_price(), removed.getTrade_volume()); + } + //초기엔 들어온 갯수에 따라 증가시켜서 계산함 + ticksQueue.add(tick); + calculator.recordTrade(tick.getTrade_price(),tick.getTrade_volume()); + //갯수 만큼 계산하기 때문에 정상 작동 +// calculator.getVWAP(); + } + + + //n초마다 5회 주문 , api 체결 내역에서 10종목씩 비교 + public double getAvgVolumePerOrder() { + return calculator.getTotalVolume() / 50; + }//todo 에러 인젝션으로 50일때와 5일때 복귀 속도 알아보기 + + public double getVWAP(){ + return calculator.getVWAP(); + } + + public int getTickSize() { + return ticksQueue.size(); + } + +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/PlatformVWAPState.java b/src/main/java/com/cleanengine/coin/realitybot/domain/PlatformVWAPState.java new file mode 100644 index 00000000..b9deb3b4 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/PlatformVWAPState.java @@ -0,0 +1,33 @@ +package com.cleanengine.coin.realitybot.domain; + +import com.cleanengine.coin.trade.entity.Trade; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Getter +@Setter +@Slf4j +public class PlatformVWAPState { + + public PlatformVWAPState(String ticker) { + this.ticker = ticker; + } + + private String ticker; + private final VWAPCalculator calculator = new VWAPCalculator(); +// private final Queue tradeQueue = new LinkedList<>(); //테스트를 위한 큐 -> 체결 db에서 데이터 조회 +// private int maxQueueSize = 10; + + public void addTrades(List trades) { + for (Trade trade : trades) { + double price = trade.getPrice(); + double volume = trade.getSize(); + calculator.recordTrade(price,volume); + } + } + public double getVWAP(){ + return calculator.getVWAP(); + }} diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPCalculator.java b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPCalculator.java new file mode 100644 index 00000000..9afb29e7 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPCalculator.java @@ -0,0 +1,25 @@ +package com.cleanengine.coin.realitybot.domain; + + +import lombok.Getter; + +@Getter +public class VWAPCalculator { + private double totalPriceVolume; + private double totalVolume; + + public void recordTrade(double price, double volume) { + totalPriceVolume += price * volume; + totalVolume += volume; + } + + public void removeTrade(double price, double volume) { + totalPriceVolume -= price * volume; + totalVolume -= volume; + } + + public double getVWAP() { + return (totalVolume == 0) ? 0.0 : totalPriceVolume / totalVolume; + } + +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/ApiVWAPService.java b/src/main/java/com/cleanengine/coin/realitybot/service/ApiVWAPService.java deleted file mode 100644 index e1a9b0b3..00000000 --- a/src/main/java/com/cleanengine/coin/realitybot/service/ApiVWAPService.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.cleanengine.coin.realitybot.service; - - -import com.cleanengine.coin.realitybot.dto.Ticks; - -import java.util.LinkedList; -import java.util.Queue; - - -public class ApiVWAPService { - private final Queue ticksQueue = new LinkedList<>(); - private double vwap; - private double totalPriceVolume; - private double totalVolume; - - public void addTick(Ticks tick){ - if (ticksQueue.size() >= 10) { - //10개 이상이 되면 선착순으로 제거해나감 - Ticks removed = ticksQueue.poll(); - totalPriceVolume -= removed.getTrade_price() * removed.getTrade_volume(); - totalVolume -= removed.getTrade_volume(); - } - //초기엔 들어온 갯수에 따라 증가시켜서 계산함 - ticksQueue.add(tick); - totalPriceVolume += tick.getTrade_price() * tick.getTrade_volume(); - totalVolume += tick.getTrade_volume(); - //갯수 만큼 계산하기 때문에 정상 작동 - calculateVWAP(); - } - - private void calculateVWAP() { - vwap = (totalVolume == 0) ? 0.0 : totalPriceVolume / totalVolume; - } - - public double getVWAP() { - return vwap; - } - - public double getAvgVolumePerOrder() { - return totalVolume / 30.0; - } - - public int getTickSize() { - return ticksQueue.size(); - } - -} 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 d3b0821f..b6544433 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java @@ -1,6 +1,6 @@ package com.cleanengine.coin.realitybot.service; -import com.cleanengine.coin.realitybot.vo.VWAPState; +import com.cleanengine.coin.realitybot.domain.PlatformVWAPState; import com.cleanengine.coin.trade.entity.Trade; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -12,15 +12,15 @@ @Service @Slf4j public class PlatformVWAPService {//TODO 가상 시장 조회용 사라질 예정임 - Map vwapMap = new ConcurrentHashMap<>(); + Map vwapMap = new ConcurrentHashMap<>(); public double calculateVWAPbyTrades(String ticker,List trades,double apiVWAP) { - VWAPState state = vwapMap.computeIfAbsent(ticker, VWAPState::new); + PlatformVWAPState state = vwapMap.computeIfAbsent(ticker, PlatformVWAPState::new); if (trades.size() < 10){ //체결 내역이 10개 이하일 경우 자체 계산 return generateVWAP(apiVWAP); } - state.calculateVWAPbyTrades(trades); + state.addTrades(trades); return state.getVWAP(); } diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/TickServiceManager.java b/src/main/java/com/cleanengine/coin/realitybot/service/TickServiceManager.java index a49ab9c6..d65f5b3d 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/TickServiceManager.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/TickServiceManager.java @@ -1,5 +1,6 @@ package com.cleanengine.coin.realitybot.service; +import com.cleanengine.coin.realitybot.domain.APIVWAPState; import org.springframework.stereotype.Service; import java.util.Map; @@ -11,8 +12,8 @@ public class TickServiceManager { * 초기엔 전역에서 vwap을 계산하거나 sequentialid를 변수에 담았으나 인스턴스가 종목별로 생성되어야 해서 작성되었습니다. * ConcurrentHashMap을 통해 중복 검사 후 종목명으로 만들어진 게 없다면 새로 만듭니다. * */ - private final Map tickServiceMap = new ConcurrentHashMap<>(); - public ApiVWAPService getService(String ticker) { - return tickServiceMap.computeIfAbsent(ticker, t -> new ApiVWAPService()); + private final Map tickServiceMap = new ConcurrentHashMap<>(); + public APIVWAPState getService(String ticker) { + return tickServiceMap.computeIfAbsent(ticker, t -> new APIVWAPState()); } } diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java b/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java deleted file mode 100644 index ae5064f5..00000000 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.cleanengine.coin.realitybot.vo; - -import com.cleanengine.coin.trade.entity.Trade; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -@Getter -@Setter -@Slf4j -public class VWAPState { - - public VWAPState(String ticker) { - this.ticker = ticker; - } - - private String ticker; -// private final Queue tradeQueue = new LinkedList<>(); //테스트를 위한 큐 -> 체결 db에서 데이터 조회 -// private int maxQueueSize = 10; - - private double totalPriceVolume = 0; - private double totalVolume = 0; - private double vwap = 0; - - - //이건 처음에나 필요했지 queue나 10개씩 받아오면서 필요 없는 로직이 되어버림 - public void recordTrade(double price, double volume) { -// tradeQueue.offer(new Vwap(price, volume)); //오로지 계산에만 목적을 둠 - totalPriceVolume += price * volume; - totalVolume += volume; - } - - public double getVWAP() { - vwap = totalVolume == 0 ? 0.0 : totalPriceVolume / totalVolume; - return vwap; - } - - public void calculateVWAPbyTrades(List trades) { - for (Trade trade : trades) { - double price = trade.getPrice(); - double volume = trade.getSize(); - recordTrade(price,volume); - } - getVWAP(); - } -} From 3276b3346ea2467a8b518d6c8580c48b09e89348 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Mon, 2 Jun 2025 17:49:37 +0900 Subject: [PATCH 16/31] =?UTF-8?q?test:=20=EC=8B=9C=EC=84=B8=20=EC=88=98?= =?UTF-8?q?=EC=A7=91=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/dto/Ticks.java | 4 - .../realitybot/RealitybotCoreTestSuite.java | 18 ++- .../coin/realitybot/api/ApiSchedulerTest.java | 12 +- .../realitybot/api/BithumbAPIClientTest.java | 31 ++++- .../config/SchedulerConfigTest.java | 15 +-- .../realitybot/domain/APIVWAPStateTest.java | 73 +++++++++++ .../domain/PlatformVWAPStateTest.java | 52 ++++++++ .../realitybot/domain/VWAPCalculatorTest.java | 92 ++++++++++++++ .../coin/realitybot/dto/TicksTest.java | 43 +++++++ .../service/OrderGenerateServiceTest.java | 7 + .../service/PlatformVWAPServiceTest.java | 12 +- .../service/TickServiceManagerTest.java | 13 +- .../coin/realitybot/vo/VWAPStateTest.java | 120 ------------------ 13 files changed, 330 insertions(+), 162 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/realitybot/domain/APIVWAPStateTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/domain/PlatformVWAPStateTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/domain/VWAPCalculatorTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/dto/TicksTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/service/OrderGenerateServiceTest.java delete mode 100644 src/test/java/com/cleanengine/coin/realitybot/vo/VWAPStateTest.java 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 aaa0f3fb..9513aab6 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java +++ b/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java @@ -2,10 +2,6 @@ import lombok.*; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalTime; - @Getter @Setter @Builder diff --git a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java index cded1567..b0ab038b 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java +++ b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java @@ -1,14 +1,19 @@ package com.cleanengine.coin.realitybot; -import com.cleanengine.coin.realitybot.api.*; +import com.cleanengine.coin.realitybot.api.ApiSchedulerTest; +import com.cleanengine.coin.realitybot.api.BithumbAPIClientTest; +import com.cleanengine.coin.realitybot.api.RefresherRunnerTest; +import com.cleanengine.coin.realitybot.api.UnitPriceRefresherTest; import com.cleanengine.coin.realitybot.config.ApiClientConfigTest; import com.cleanengine.coin.realitybot.config.SchedulerConfigTest; +import com.cleanengine.coin.realitybot.domain.APIVWAPStateTest; +import com.cleanengine.coin.realitybot.domain.PlatformVWAPStateTest; +import com.cleanengine.coin.realitybot.domain.VWAPCalculatorTest; import com.cleanengine.coin.realitybot.dto.OpeningPriceTest; +import com.cleanengine.coin.realitybot.dto.TicksTest; import com.cleanengine.coin.realitybot.service.PlatformVWAPServiceTest; -import com.cleanengine.coin.realitybot.service.TickServiceManager; import com.cleanengine.coin.realitybot.service.TickServiceManagerTest; import com.cleanengine.coin.realitybot.vo.UnitPricePolicyTest; -import com.cleanengine.coin.realitybot.vo.VWAPStateTest; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @@ -18,13 +23,16 @@ BithumbAPIClientTest.class, UnitPriceRefresherTest.class, OpeningPriceTest.class, - VWAPStateTest.class, + PlatformVWAPStateTest.class, UnitPricePolicyTest.class, SchedulerConfigTest.class, ApiSchedulerTest.class, ApiClientConfigTest.class, PlatformVWAPServiceTest.class, - TickServiceManagerTest.class + TickServiceManagerTest.class, + VWAPCalculatorTest.class, + APIVWAPStateTest.class, + TicksTest.class }) public class RealitybotCoreTestSuite { } 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 946a847b..31dedd92 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java @@ -4,7 +4,7 @@ import com.cleanengine.coin.order.infra.AssetRepository; import com.cleanengine.coin.realitybot.dto.Ticks; import com.cleanengine.coin.realitybot.parser.TickParser; -import com.cleanengine.coin.realitybot.service.ApiVWAPService; +import com.cleanengine.coin.realitybot.domain.APIVWAPState; import com.cleanengine.coin.realitybot.service.OrderGenerateService; import com.cleanengine.coin.realitybot.service.TickServiceManager; import org.junit.jupiter.api.Test; @@ -14,8 +14,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import static org.mockito.Mockito.*; @@ -32,12 +30,10 @@ public class ApiSchedulerTest { @Mock private OrderGenerateService orderGenerateService; @Mock - ApiVWAPService apiVWAPService; + APIVWAPState apiVWAPState; @Mock private TickServiceManager tickServiceManager; @Mock - private Map lastSequentialIdMap = new ConcurrentHashMap<>(); - @Mock private AssetRepository assetRepository; @@ -65,8 +61,8 @@ void marketAllRequestCallsAllTickers() throws InterruptedException { when(assetRepository.findAll()).thenReturn(assets); when(apiClient.get(anyString())).thenReturn("[{data:...}]"); when(tickParser.parseGson(anyString())).thenReturn(testTicks); - when(tickServiceManager.getService(anyString())).thenReturn(apiVWAPService); - doNothing().when(apiVWAPService).addTick(any()); + when(tickServiceManager.getService(anyString())).thenReturn(apiVWAPState); + doNothing().when(apiVWAPState).addTick(any()); System.out.println(assets.size()); //when apiScheduler.MarketAllRequest(); diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java index e03b2b60..6e46bdef 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java @@ -102,7 +102,7 @@ void callOpeningPrice() throws IOException { Response mockresponse = new Response.Builder() .request(mockrequest) .protocol(Protocol.HTTP_1_1) - .code(400) + .code(200) .message("OK") .body(responseBody) .build(); @@ -127,7 +127,7 @@ void callFailbyWrongTicker() throws IOException { Response mockresponse = new Response.Builder() .request(mockrequest) .protocol(Protocol.HTTP_1_1) - .code(200) + .code(400) .message("OK") .body(responseBody) .build(); @@ -164,6 +164,31 @@ void callTradePriceFails() throws IOException { when(call.execute()).thenThrow(new IOException("API 요청 중 예외 발생")); //then - assertThrows(RuntimeException.class, () -> bithumbAPIClient.getOpeningPrice(ticker)); + assertThrows(RuntimeException.class, () -> bithumbAPIClient.get(ticker)); + } + + @DisplayName("ticker가 잘못된 요청이 들어갔을 때 log를 띄우는 지") + @Test + void callFailbyWrongTickertoGet() throws IOException { + //given + + ResponseBody responseBody = ResponseBody.create(failJson, MediaType.get("application/json")); + Request mockrequest = new Request.Builder().url("http://localhost").build(); + Response mockresponse = new Response.Builder() + .request(mockrequest) + .protocol(Protocol.HTTP_1_1) + .code(400) + .message("OK") + .body(responseBody) + .build(); + + when(client.newCall(any())).thenReturn(call); + when(call.execute()).thenReturn(mockresponse); + + //when + String response = bithumbAPIClient.get(ticker); + + //then + assertTrue(response.contains("{}")); } } \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/config/SchedulerConfigTest.java b/src/test/java/com/cleanengine/coin/realitybot/config/SchedulerConfigTest.java index 59b4fd78..3b573a5a 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/config/SchedulerConfigTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/config/SchedulerConfigTest.java @@ -10,12 +10,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.scheduling.config.ScheduledTaskRegistrar; -import org.springframework.scheduling.config.Task; -import java.lang.reflect.Field; import java.time.Duration; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; @@ -33,16 +32,14 @@ public class SchedulerConfigTest { SchedulerConfig schedulerConfig; @BeforeEach - void setUp() throws NoSuchFieldException, IllegalAccessException { - Field fixedRateField = SchedulerConfig.class.getDeclaredField("fixedRate"); - fixedRateField.setAccessible(true); - fixedRateField.set(schedulerConfig, Duration.ofSeconds(1)); + void setUp() { + schedulerConfig = new SchedulerConfig(apiScheduler,Duration.ofMillis(500)); } @DisplayName("fixedrate를 적용 후 정상 작동하는 지") @Test - void testConfigureTasksOnFixedRate() throws NoSuchFieldException, IllegalAccessException, InterruptedException { + void testConfigureTasksOnFixedRate() throws InterruptedException { //when schedulerConfig.configureTasks(scheduledTaskRegistrar); @@ -62,7 +59,7 @@ void testConfigureTasksOnFixedRate() throws NoSuchFieldException, IllegalAccessE verify(apiScheduler).MarketAllRequest(); //작동 검증 Duration interval = intervalCaptor.getValue(); - assertEquals(Duration.ofSeconds(1), interval); + assertEquals(Duration.ofMillis(500), interval); } @DisplayName("marketallrequest가 예외 발생 시 에러를 던지는 지 확인") @Test diff --git a/src/test/java/com/cleanengine/coin/realitybot/domain/APIVWAPStateTest.java b/src/test/java/com/cleanengine/coin/realitybot/domain/APIVWAPStateTest.java new file mode 100644 index 00000000..adb1b450 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/domain/APIVWAPStateTest.java @@ -0,0 +1,73 @@ +package com.cleanengine.coin.realitybot.domain; + +import com.cleanengine.coin.realitybot.dto.Ticks; +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 static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +public class APIVWAPStateTest { + + @Mock + VWAPCalculator calculator; + @InjectMocks + APIVWAPState apivwapState; + + @DisplayName("ticks가 10개 이하 일 경우 ticks 갯수만큼 record 작동") + @Test + void testAddTicksUnder10() { + //given + for (int i = 0; i < 5; i++) { + apivwapState.addTick(new Ticks("BTC","2025-06-01","11:32:45","2025-06-01T11:32:45.789Z",100,i,180.0f,5.5,"ASK", 100003L)); + } + //when + + //then + assertEquals(5,apivwapState.getTickSize()); + } + + @DisplayName("ticks가 10개 초과 시 오래된 tick 제거 후 size 유지") + @Test + void testAddTicksOver10(){ + APIVWAPState apivwapState = new APIVWAPState();//mock이 아니라 진짜 객체 생성 + //given + Ticks firstTick = new Ticks("BTC","2025-06-01","11:32:45","2025-06-01T11:32:45.789Z",1000,10,180.0f,5.5,"ASK", 100003L); + apivwapState.addTick(firstTick); + for (int i = 0; i < 10; i++) { + apivwapState.addTick(new Ticks("BTC","2025-06-01","11:32:45","2025-06-01T11:32:45.789Z",100,i,180.0f,5.5,"ASK", 100003L)); + } + assertEquals(100,apivwapState.getVWAP()); + + Ticks lastTick = new Ticks("BTC","2025-06-01","11:32:45","2025-06-01T11:32:45.789Z",999,9,180.0f,5.5,"ASK", 100003L); + apivwapState.addTick(lastTick); + //when + + //then + assertEquals(10,apivwapState.getTickSize()); + assertEquals(249.83,apivwapState.getVWAP(),0.1); + } + + + @DisplayName("평균 주문 갯수로 계산한다.") + @Test + void testGetAvgVolume(){ + APIVWAPState apivwapState = new APIVWAPState();//mock이 아니라 진짜 객체 생성 + //given + Ticks firstTick = new Ticks("BTC","2025-06-01","11:32:45","2025-06-01T11:32:45.789Z",1000,10,180.0f,5.5,"ASK", 100003L); + apivwapState.addTick(firstTick); + for (int i = 0; i < 10; i++) { + apivwapState.addTick(new Ticks("BTC","2025-06-01","11:32:45","2025-06-01T11:32:45.789Z",i,10,180.0f,5.5,"ASK", 100003L)); + } + Ticks lastTick = new Ticks("BTC","2025-06-01","11:32:45","2025-06-01T11:32:45.789Z",999,10,180.0f,5.5,"ASK", 100003L); + apivwapState.addTick(lastTick); + //when + + //then + assertEquals(2,apivwapState.getAvgVolumePerOrder()); + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/domain/PlatformVWAPStateTest.java b/src/test/java/com/cleanengine/coin/realitybot/domain/PlatformVWAPStateTest.java new file mode 100644 index 00000000..244ec318 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/domain/PlatformVWAPStateTest.java @@ -0,0 +1,52 @@ +package com.cleanengine.coin.realitybot.domain; + +import com.cleanengine.coin.trade.entity.Trade; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PlatformVWAPStateTest { + +/* @DisplayName("10개 이상의 거래를 보낼 경우 최신 10개만 계산하는 지") + @Test + void testVWAPStacksOver10Trades(){ + String ticker = "BTC"; + VWAPState vwapState = new VWAPState(ticker); + for (int i = 1; i < 16; i++) { + double price = i*100; + System.out.println(price); + vwapState.recordTrade(price,1); + } + System.out.println(vwapState.getTotalPriceVolume()); + System.out.println(vwapState.getTotalVolume()); + System.out.println(vwapState.getVWAP()); + assertEquals(vwapState.getVWAP(), 0); + }*/ + + @DisplayName("10개 이상의 모든 거래를 계산한다.") + @Test + void TestcalculateVWAPbyTrades() { + String ticker = "BTC"; + PlatformVWAPState platformVwapState = new PlatformVWAPState(ticker); + List trades = List.of( + new Trade(1, "BTC", LocalDateTime.now(), 2, 1, 10000.0, 10.0), // 100000 + new Trade(2, "BTC", LocalDateTime.now(), 2, 1, 11000.0, 10.0), // 110000 + new Trade(3, "BTC", LocalDateTime.now(), 2, 1, 12000.0, 10.0), // 120000 + new Trade(4, "BTC", LocalDateTime.now(), 2, 1, 13000.0, 10.0), // 130000 + new Trade(5, "BTC", LocalDateTime.now(), 2, 1, 14000.0, 10.0), // 140000 + new Trade(6, "BTC", LocalDateTime.now(), 2, 1, 15000.0, 10.0), // 150000 + new Trade(7, "BTC", LocalDateTime.now(), 2, 1, 16000.0, 10.0), // 160000 + new Trade(8, "BTC", LocalDateTime.now(), 2, 1, 17000.0, 10.0), // 170000 + new Trade(9, "BTC", LocalDateTime.now(), 2, 1, 18000.0, 10.0), // 180000 + new Trade(10, "BTC", LocalDateTime.now(), 2, 1, 19000.0, 10.0), // 190000 + new Trade(11, "BTC", LocalDateTime.now(), 2, 1, 20000.0, 10.0) // 200000 + ); + platformVwapState.addTrades(trades); + System.out.println(platformVwapState.getVWAP()); + assertEquals(platformVwapState.getVWAP(),15000.0); + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/domain/VWAPCalculatorTest.java b/src/test/java/com/cleanengine/coin/realitybot/domain/VWAPCalculatorTest.java new file mode 100644 index 00000000..0320b493 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/domain/VWAPCalculatorTest.java @@ -0,0 +1,92 @@ +package com.cleanengine.coin.realitybot.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class VWAPCalculatorTest { + + private final VWAPCalculator calculator = new VWAPCalculator(); + + @DisplayName("trade 한 건으로 vwap 계산한다.") + @Test + void testVWAPwithSingleTrade() { + + calculator.recordTrade(18000.0,10.0); + assertEquals(calculator.getTotalPriceVolume(), 180000.0); + assertEquals(calculator.getTotalVolume(), 10.0); + assertEquals(calculator.getVWAP(), 18000.0); + } + @DisplayName("수량이 0 일 경우 계산되지 않는다.") + @Test + void testVWAPwith0VolumeTrade() { + calculator.recordTrade(18000.0,0); + assertEquals(calculator.getTotalPriceVolume(), 0); + assertEquals(calculator.getTotalVolume(), 0); + assertEquals(calculator.getVWAP(), 0); + } + @DisplayName("금액이 0원일 경우 계산되지 않는다.") + @Test + void testVWAPwith0PriceTrade() { + String ticker = "BTC"; + PlatformVWAPState platformVwapState = new PlatformVWAPState(ticker); + calculator.recordTrade(0,100); + assertEquals(calculator.getTotalPriceVolume(), 0); + assertEquals(calculator.getTotalVolume(), 100); + assertEquals(calculator.getVWAP(), 0); + } + @DisplayName("trade 건별로 누적되어 vwap 계산한다.") + @Test + void testVWAPStackTrades() { + calculator.recordTrade(18000, 100); + calculator.recordTrade(17000, 70); + calculator.recordTrade(16000, 50); + calculator.recordTrade(15000, 30); + calculator.recordTrade(14000, 10); + assertEquals(calculator.getTotalPriceVolume(), 4380000.0); + assertEquals(calculator.getTotalVolume(), 260.0); + assertEquals(calculator.getVWAP(), 16846.1538, 0.0001); + } + @DisplayName("수량이 0일 경우 체결 건이 있어도 적용되지 않는다.") + @Test + void testVWAPStackTradesWith0Volumes() { + calculator.recordTrade(18000, 0); + calculator.recordTrade(17000, 0); + calculator.recordTrade(16000, 0); + calculator.recordTrade(15000, 0); + calculator.recordTrade(14000, 0); + assertEquals(calculator.getTotalPriceVolume(), 0); + assertEquals(calculator.getTotalVolume(), 0); + assertEquals(calculator.getVWAP(), 0.0); + } + @DisplayName("여러 체결 건 중 한 건만 수량이 있을 경우 그 건만 적용된다.") + @Test + void testVWAPStack1Trades(){ + calculator.recordTrade(18000,0); + calculator.recordTrade(17000,0); + calculator.recordTrade(16000,0); + calculator.recordTrade(15000,0); + calculator.recordTrade(14000,1); + assertEquals(calculator.getTotalPriceVolume(), 14000.0); + assertEquals(calculator.getTotalVolume(), 1); + assertEquals(calculator.getVWAP(), 14000.0); + } + @DisplayName("쌓인 주문에서 일부를 제거한다.") + @Test + void testVWAPRemoveTrades() { + calculator.recordTrade(18000, 100); + calculator.recordTrade(17000, 70); + calculator.recordTrade(16000, 50); + calculator.recordTrade(15000, 30); + calculator.recordTrade(14000, 10); + calculator.removeTrade(18000, 100); + calculator.removeTrade(17000, 70); + calculator.removeTrade(16000, 50); + calculator.removeTrade(15000, 30); + assertEquals(calculator.getTotalPriceVolume(), 140000.0); + assertEquals(calculator.getTotalVolume(), 10.0); + assertEquals(calculator.getVWAP(), 14000.0); + } + +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/dto/TicksTest.java b/src/test/java/com/cleanengine/coin/realitybot/dto/TicksTest.java new file mode 100644 index 00000000..a3d3a5c9 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/dto/TicksTest.java @@ -0,0 +1,43 @@ +package com.cleanengine.coin.realitybot.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TicksTest { + @Test + void testToString() { + // given + Ticks tick = Ticks.builder() + .market("KRW-BTC") + .trade_date_utc("2025-06-01") + .trade_time_utc("11:32:45") + .timestamp("2025-06-01T11:32:45.789Z") + .trade_price(1000.0f) + .trade_volume(5.0) + .prev_closing_price(980.0f) + .change_price(20.0) + .ask_bid("ASK") + .sequential_id(1000001L) + .build(); + + // when + String actual = tick.toString(); + + // then + String expected = "Ticks{" + + "market='KRW-BTC', " + + "trade_date_utc='2025-06-01', " + + "trade_time_utc='11:32:45', " + + "timestamp=2025-06-01T11:32:45.789Z, " + + "trade_price=1000.0, " + + "trade_volume=5.0, " + + "prev_closing_price=980.0, " + + "change_price=20.0, " + + "ask_bid='ASK', " + + "sequential_id=1000001" + + "}"; + + assertEquals(expected, actual); + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/OrderGenerateServiceTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/OrderGenerateServiceTest.java new file mode 100644 index 00000000..913df305 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/service/OrderGenerateServiceTest.java @@ -0,0 +1,7 @@ +package com.cleanengine.coin.realitybot.service; + +import static org.junit.jupiter.api.Assertions.*; + +public class OrderGenerateServiceTest { + +} \ 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 ad262f4e..b4138452 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java @@ -1,6 +1,6 @@ package com.cleanengine.coin.realitybot.service; -import com.cleanengine.coin.realitybot.vo.VWAPState; +import com.cleanengine.coin.realitybot.domain.PlatformVWAPState; import com.cleanengine.coin.trade.entity.Trade; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,7 +23,7 @@ public class PlatformVWAPServiceTest { private PlatformVWAPService platformVWAPService; @Mock - private VWAPState vwapState; + private PlatformVWAPState platformVwapState; @DisplayName("10개 이하일 때, APIVWAP 기준으로 랜덤값이 반환되는 지") @Test @@ -64,14 +64,14 @@ void testCalculateVWAPMoreThan10Trades() { new Trade(11, "BTC", LocalDateTime.now(), 2, 1, 20000.0, 10.0) // 200000 ); double apiVWAP = 1000.0; - when(vwapState.getVWAP()).thenReturn(15000.0); - platformVWAPService.vwapMap.put(ticker,vwapState); + when(platformVwapState.getVWAP()).thenReturn(15000.0); + platformVWAPService.vwapMap.put(ticker, platformVwapState); //when double result = platformVWAPService.calculateVWAPbyTrades(ticker, trades, apiVWAP); //then - verify(vwapState).calculateVWAPbyTrades(trades); - verify(vwapState).getVWAP(); + verify(platformVwapState).addTrades(trades); + verify(platformVwapState).getVWAP(); assertEquals( 15000.0,result); } //todo generatevwap null확인안함 diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/TickServiceManagerTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/TickServiceManagerTest.java index 7ca9b9b8..3e69d393 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/service/TickServiceManagerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/TickServiceManagerTest.java @@ -1,9 +1,8 @@ package com.cleanengine.coin.realitybot.service; +import com.cleanengine.coin.realitybot.domain.APIVWAPState; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; import static org.junit.jupiter.api.Assertions.*; @@ -17,7 +16,7 @@ void getNewService() { //given String ticker = "BTC"; //when - ApiVWAPService service = tickServiceManager.getService(ticker); + APIVWAPState service = tickServiceManager.getService(ticker); //then assertNotNull(service); } @@ -27,8 +26,8 @@ void checksDuplication() { //given String ticker = "BTC"; //when - ApiVWAPService service1 = tickServiceManager.getService(ticker); - ApiVWAPService service2 = tickServiceManager.getService(ticker); + APIVWAPState service1 = tickServiceManager.getService(ticker); + APIVWAPState service2 = tickServiceManager.getService(ticker); //then assertSame(service1, service2); } @@ -38,8 +37,8 @@ void checksDuplication() { void checksOthers() { //given //when - ApiVWAPService service1 = tickServiceManager.getService("BTC"); - ApiVWAPService service2 = tickServiceManager.getService("TRUMP"); + APIVWAPState service1 = tickServiceManager.getService("BTC"); + APIVWAPState service2 = tickServiceManager.getService("TRUMP"); //then assertNotSame(service1, service2); } diff --git a/src/test/java/com/cleanengine/coin/realitybot/vo/VWAPStateTest.java b/src/test/java/com/cleanengine/coin/realitybot/vo/VWAPStateTest.java deleted file mode 100644 index 76a57577..00000000 --- a/src/test/java/com/cleanengine/coin/realitybot/vo/VWAPStateTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.cleanengine.coin.realitybot.vo; - -import com.cleanengine.coin.trade.entity.Trade; -import org.junit.jupiter.api.Test; - -import java.time.LocalDateTime; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class VWAPStateTest { - - - @Test - void testVWAPwithSingleTrade() { - String ticker = "BTC"; - VWAPState vwapState = new VWAPState(ticker); - vwapState.recordTrade(18000.0,10.0); - assertEquals(vwapState.getTotalPriceVolume(), 180000.0); - assertEquals(vwapState.getTotalVolume(), 10.0); - assertEquals(vwapState.getVWAP(), 18000.0); - } - @Test - void testVWAPwith0VolumeTrade() { - String ticker = "BTC"; - VWAPState vwapState = new VWAPState(ticker); - vwapState.recordTrade(18000.0,0); - assertEquals(vwapState.getTotalPriceVolume(), 0); - assertEquals(vwapState.getTotalVolume(), 0); - assertEquals(vwapState.getVWAP(), 0); - } - @Test - void testVWAPwith0PriceTrade() { - String ticker = "BTC"; - VWAPState vwapState = new VWAPState(ticker); - vwapState.recordTrade(0,100); - assertEquals(vwapState.getTotalPriceVolume(), 0); - assertEquals(vwapState.getTotalVolume(), 100); - assertEquals(vwapState.getVWAP(), 0); - } - @Test - void testVWAPStackTrades(){ - String ticker = "BTC"; - VWAPState vwapState = new VWAPState(ticker); - vwapState.recordTrade(18000,100); - vwapState.recordTrade(17000,70); - vwapState.recordTrade(16000,50); - vwapState.recordTrade(15000,30); - vwapState.recordTrade(14000,10); - assertEquals(vwapState.getTotalPriceVolume(), 4380000.0); - assertEquals(vwapState.getTotalVolume(), 260.0); - assertEquals(vwapState.getVWAP(), 16846.1538,0.0001); - } - @Test - void testVWAPStackTradesWith0Volumes(){ - String ticker = "BTC"; - VWAPState vwapState = new VWAPState(ticker); - vwapState.recordTrade(18000,0); - vwapState.recordTrade(17000,0); - vwapState.recordTrade(16000,0); - vwapState.recordTrade(15000,0); - vwapState.recordTrade(14000,0); - assertEquals(vwapState.getTotalPriceVolume(), 0); - assertEquals(vwapState.getTotalVolume(), 0); - assertEquals(vwapState.getVWAP(), 0.0); - } - @Test - void testVWAPStack1Trades(){ - String ticker = "BTC"; - VWAPState vwapState = new VWAPState(ticker); - vwapState.recordTrade(18000,0); - vwapState.recordTrade(17000,0); - vwapState.recordTrade(16000,0); - vwapState.recordTrade(15000,0); - vwapState.recordTrade(14000,1); - assertEquals(vwapState.getTotalPriceVolume(), 14000.0); - assertEquals(vwapState.getTotalVolume(), 1); - assertEquals(vwapState.getVWAP(), 14000.0); - } -/* @DisplayName("10개 이상의 거래를 보낼 경우 최신 10개만 계산하는 지") - @Test - void testVWAPStacksOver10Trades(){ - String ticker = "BTC"; - VWAPState vwapState = new VWAPState(ticker); - for (int i = 1; i < 16; i++) { - double price = i*100; - System.out.println(price); - vwapState.recordTrade(price,1); - } - System.out.println(vwapState.getTotalPriceVolume()); - System.out.println(vwapState.getTotalVolume()); - System.out.println(vwapState.getVWAP()); - assertEquals(vwapState.getVWAP(), 0); - }*/ - @Test - void getVWAP() { - } - - @Test - void TestcalculateVWAPbyTrades() { - String ticker = "BTC"; - VWAPState vwapState = new VWAPState(ticker); - List trades = List.of( - new Trade(1, "BTC", LocalDateTime.now(), 2, 1, 10000.0, 10.0), // 100000 - new Trade(2, "BTC", LocalDateTime.now(), 2, 1, 11000.0, 10.0), // 110000 - new Trade(3, "BTC", LocalDateTime.now(), 2, 1, 12000.0, 10.0), // 120000 - new Trade(4, "BTC", LocalDateTime.now(), 2, 1, 13000.0, 10.0), // 130000 - new Trade(5, "BTC", LocalDateTime.now(), 2, 1, 14000.0, 10.0), // 140000 - new Trade(6, "BTC", LocalDateTime.now(), 2, 1, 15000.0, 10.0), // 150000 - new Trade(7, "BTC", LocalDateTime.now(), 2, 1, 16000.0, 10.0), // 160000 - new Trade(8, "BTC", LocalDateTime.now(), 2, 1, 17000.0, 10.0), // 170000 - new Trade(9, "BTC", LocalDateTime.now(), 2, 1, 18000.0, 10.0), // 180000 - new Trade(10, "BTC", LocalDateTime.now(), 2, 1, 19000.0, 10.0), // 190000 - new Trade(11, "BTC", LocalDateTime.now(), 2, 1, 20000.0, 10.0) // 200000 - ); - vwapState.calculateVWAPbyTrades(trades); - System.out.println(vwapState.getVWAP()); - assertEquals(vwapState.getVWAP(),15000.0); - } -} \ No newline at end of file From e51cb5b09375441c2ad08ef8e806ba7364364294 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Wed, 4 Jun 2025 04:21:04 +0900 Subject: [PATCH 17/31] =?UTF-8?q?feat:=20=EC=84=A0=ED=98=95=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=84=20=EC=9E=91=EC=97=85=20-=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EC=84=A0=ED=98=95=20=EB=B3=B4=EA=B0=84=20=EB=B0=8F=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/OrderGenerateService.java | 131 +++--------------- .../service/VWAPerrorInJectionScheduler.java | 2 +- .../realitybot/vo/DeviationPricePolicy.java | 70 ++++++++++ .../coin/realitybot/vo/OrderPricePolicy.java | 61 ++++++++ .../coin/realitybot/vo/OrderVolumePolicy.java | 54 ++++++++ 5 files changed, 209 insertions(+), 109 deletions(-) create mode 100644 src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java 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 82ab9253..a5046751 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -5,6 +5,9 @@ import com.cleanengine.coin.order.external.adapter.account.AccountExternalRepository; import com.cleanengine.coin.order.external.adapter.wallet.WalletExternalRepository; import com.cleanengine.coin.realitybot.api.UnitPriceRefresher; +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.trade.entity.Trade; import com.cleanengine.coin.trade.repository.TradeRepository; import com.cleanengine.coin.user.domain.Account; @@ -37,6 +40,9 @@ public class OrderGenerateService { private final VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler; private final WalletExternalRepository walletExternalRepository; private final AccountExternalRepository accountExternalRepository; + private final OrderPricePolicy orderPricePolicy; + private final DeviationPricePolicy deviationPricePolicy; + private final OrderVolumePolicy orderVolumePolicy; private String ticker; @@ -55,91 +61,29 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// //편차 계산 (vwap 기준) double trendLineRate = (platformVWAP - apiVWAP)/ apiVWAP; - //편차가 +-1% 이상 발생하면 true 반환 - boolean isWithinRange = Math.abs(trendLineRate) <= 0.001; //TODO 호가 단위에 따른 편차 보정 필요 for(int level : orderLevels) { //1주문당 3회 매수매도 처리 - double priceOffset = unitPrice * level; //호가 단위만큼 단계별 offset 설정 - //randomoffset는 1단계 밀집 주문을 위해 offset 편차가 많이 안나도록 동적으로 max를 제한함 - double randomOffset = Math.abs(level1TradeMaker(platformVWAP,getDynamicMaxRate(trendLineRate))); - double deviation = Math.abs(trendLineRate); //편차 구하기 - double sellPrice; - double buyPrice; - - //1단계 밀집 주문 - if (level == 1){ //1level일 경우 주문이 겹치도록 설정 - double basePrice = normalizeToUnit(platformVWAP); //기준 가격 (호가 단위 정규화) - //체결을 위해 매수가 올리고, 매도가 내리는 계산 적용 - sellPrice = normalizeToUnit(basePrice - randomOffset); - buyPrice = normalizeToUnit(basePrice + randomOffset); - } - //2~3 단계 : orderbook 단위 주문 - else { - randomOffset = level1TradeMaker(platformVWAP,0.01); - //체결 확률 증가용 코드 - sellPrice = normalizeToUnit(platformVWAP + priceOffset - randomOffset); - buyPrice = normalizeToUnit(platformVWAP - priceOffset + randomOffset); - //안정적인 스프레드 유지 -// sellPrice = normalizeToUnit(platformVWAP + priceOffset); -// buyPrice = normalizeToUnit(platformVWAP - priceOffset); - } + OrderPricePolicy.OrderPrice basePrice = orderPricePolicy.calculatePrice(level,platformVWAP,unitPrice,trendLineRate); + 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(); + + + createOrderWithFallback(ticker,false, sellVolume, sellPrice); + createOrderWithFallback(ticker,true, buyVolume, buyPrice); - //주문 실행 - double sellVolume = getRandomVolum(avgVolume); - double buyVolume = getRandomVolum(avgVolume); - - if (platformVWAP != 0){ - if (isWithinRange){ - if (trendLineRate > 0){ - sellVolume *=1.5; - buyVolume *= 0.7; - } else { - sellVolume *=0.7; - buyVolume *= 1.5; - } - } - double correctionRate = 0.1; - if (trendLineRate < -0.01) { // platformVWAP이 너무 낮음 - sellPrice = normalizeToUnit(sellPrice + (apiVWAP * correctionRate)); // 매도 비싸게 - buyPrice = normalizeToUnit(buyPrice + (apiVWAP * correctionRate)); // 매수 비싸게 - } else if (trendLineRate > 0.01) { // platformVWAP이 너무 높음 - sellPrice = normalizeToUnit(sellPrice - (apiVWAP * correctionRate)); // 매도 싸게 - buyPrice = normalizeToUnit(buyPrice - (apiVWAP * correctionRate)); // 매수 싸게 - //platform vwap -> vwap으로 변환 - } - - - // 편차에 따라 강도 조절 - if (deviation > 0.01) { - double power = trendLineRate * 100; // 3% → 3 - if (trendLineRate < 0) { - buyVolume *= 1.0 + Math.abs(power) * 0.5; // 3% → 2.5배 - sellVolume *= 1.0 + Math.abs(power) * 0.5; - buyPrice = normalizeToUnit(apiVWAP * (1 + 0.002 * power)); // +0.6% - sellPrice = normalizeToUnit(apiVWAP * (1 + 0.002 * power)); // +0.6% - } else { - buyVolume *= 1.0 + Math.abs(power) * 0.5; // 3% → 2.5배 - buyPrice = normalizeToUnit(apiVWAP * (1 - 0.002 * power)); // -0.6% - sellVolume *= 1.0 + Math.abs(power) * 0.5; - sellPrice = normalizeToUnit(apiVWAP * (1 - 0.002 * power)); // -0.6% - } - } - createOrderWithFallback(ticker,false, sellVolume,sellPrice); - createOrderWithFallback(ticker,true, buyVolume,buyPrice); - } else { - - //스위치 시켜야 할까? - createOrderWithFallback(ticker,false, sellVolume,sellPrice); - createOrderWithFallback(ticker,true, buyVolume,buyPrice); - } try { TimeUnit.MICROSECONDS.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } -// vwaPerrorInJectionScheduler.enableInjection(); //에러 발생기 비활성화 + 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)); @@ -149,14 +93,14 @@ 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]"); + System.out.println("📦"+ticker+" [체결 기록 Top 10]"); trades.forEach(t -> System.out.printf("🕒 %s | 가격: %.0f | 수량: %.8f | 매수: #%d ↔ 매도: #%d%n", t.getTradeTime(), t.getPrice(), t.getSize(), t.getBuyUserId(), t.getSellUserId()) - );*/ + ); } private void createOrderWithFallback(String ticker,boolean isBuy, double volume, double price ) { @@ -197,33 +141,4 @@ protected void resetBot(String ticker){ accountExternalRepository.save(account2); } - //==================================order 정규화용 ============================================ - - private double level1TradeMaker(double platformVWAP, double maxRate){ - //시장가에 해당하는 호가는 거래 체결 강하게 하기 위함 - double percent = (Math.random() * 2-1)*maxRate; - return platformVWAP * 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){ //호가단위로 변환 - return (int) ((double)(Math.round(price / unitPrice)) * unitPrice); - } - - private double getRandomVolum(double avgVolum){ //볼륨 랜덤 입력 - double rawVolume = avgVolum * (0.5+Math.random()); - //호가 단위에 따라 0원이 발생 가능성 - double resultVolume = Math.round(rawVolume * 10000.0)/10000.0; - if(resultVolume <= 0){ - //Volume이 0이하일 경우 재 계산 - resultVolume = Math.round(rawVolume * 10000000.0)/10000000.0; - } - return resultVolume; - } } diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java b/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java index 289144e5..059e24d1 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java @@ -25,7 +25,7 @@ public void enableInjection() { this.shouldInject = true; } - @Scheduled(fixedRate = 60000) // 혹은 따로 수동 호출도 가능 + @Scheduled(fixedRate = 30000) // 혹은 따로 수동 호출도 가능 public void injectFakeTrade() { if (!shouldInject || hasInjected) return; diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java new file mode 100644 index 00000000..89e6f310 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java @@ -0,0 +1,70 @@ +package com.cleanengine.coin.realitybot.vo; + +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@NoArgsConstructor +public class DeviationPricePolicy { + /** + * 편차율이 클 경우 가격과 수량을 강하게 보정합니다. + * + * @param platformSell 계산 된 플랫폼 기준 매도 가격 + * @param platformBuy 계산 된 플랫폼 기준 매수 가격 + * @param trendLineRate (platformVWAP - apiVWAP) / apiVWAP + * @param apiVWAP 외부 기준 가격 + * @return 추가 선형 보정된 가격쌍 (sell, buy) + */ + + public AdjustPrice adjust(double platformSell,double platformBuy, double trendLineRate, double apiVWAP, double unitPrice){ + double deviation = Math.abs(trendLineRate); + + if (deviation <= 0.01){ + return new AdjustPrice(platformSell,platformBuy); + } + double weight = getCorrectionWeight(deviation); + double closeness = 0.5 + (weight * 0.3); // 보간 가중치: 0.7 ~ 1.0 -> 0.5 + + double targetVWAP = (trendLineRate > 0) //만약 closeness 를 0.5 입력시 중간값 + ? apiVWAP + (platformSell - apiVWAP) * closeness // 고평가 → platformSell(25000) → apiVWAP(16000) 사이 가중치 %로 유도 + : apiVWAP - (apiVWAP - platformBuy) * closeness; // 저평가 → platformBuy(12000) ← apiVWAP(16000) 사이 가중치 %로 유도 + + double adjustedSell = normalizeToUnit(interpolate(platformSell,targetVWAP ,weight),unitPrice); + double adjustedBuy = normalizeToUnit(interpolate(platformBuy,targetVWAP ,weight),unitPrice); + + return new AdjustPrice(adjustedSell,adjustedBuy); + + } + + /*private double getCorrentionRate(double deviation) { 3단계 보정에서 선형보정 + if (deviation <= 0.01){ + return 0.05; //5% 약보정 + } else if (deviation <= 0.03){ + return 0.10; //10% 의 중보정 + } else return 0.15; //15%의 강보정 + }*/ + + /** + * 1% 미만은 보정 X, 10% 이상은 거의 전면 보정. + * 중간값은 비례적으로 weight 증가 + */ + private double getCorrectionWeight(double deviation) { + double start = 0.01; // 보정 시작 기준 (1%) + double end = 0.10; // 보정 최댓값 기준 (10%) + + double weight = (deviation - start) / (end - start); + return Math.min(1.0, Math.max(0.0, weight)); // 0 ~ 1 사이로 제한 + } + + /** + * 선형 보간 함수: platformPrice → apiVWAP 사이 보간 + */ + private double interpolate(double platformPrice, double apiVWAP, double weight) { + return platformPrice * (1 - weight) + apiVWAP * weight; + } + private double normalizeToUnit(double price, double unitPrice) { + return Math.round(price / unitPrice) * unitPrice; + } + + public record AdjustPrice(double sell, double buy){} +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java new file mode 100644 index 00000000..fc185ef2 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java @@ -0,0 +1,61 @@ +package com.cleanengine.coin.realitybot.vo; + +import org.springframework.stereotype.Component; + +@Component +public class OrderPricePolicy { + /** + * 레벨에 따라 매수/매도 가격을 계산합니다. + * @param level 주문 강도 (1~5) + * @param platformVWAP 플랫폼 기준 평균 체결 가격 + * @param unitPrice 호가 단위 + * @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))); + + + if (level == 1){ //1level일 경우 주문이 겹치도록 설정 + double basePrice = normalizeToUnit(platformVWAP, unitPrice); //기준 가격 (호가 단위 정규화) + //체결을 위해 매수가 올리고, 매도가 내리는 계산 적용 + sellPrice = normalizeToUnit(basePrice - randomOffset,unitPrice); + buyPrice = normalizeToUnit(basePrice + randomOffset,unitPrice); + } + //2~3 단계 : orderbook 단위 주문 + else { + randomOffset = getRandomOffset(platformVWAP,0.01); + //체결 확률 증가용 코드 + sellPrice = normalizeToUnit(platformVWAP + priceOffset - randomOffset,unitPrice); + buyPrice = normalizeToUnit(platformVWAP - priceOffset + randomOffset,unitPrice); + //안정적인 스프레드 유지 +// sellPrice = normalizeToUnit(platformVWAP + priceOffset); +// buyPrice = normalizeToUnit(platformVWAP - priceOffset); + } + return new OrderPrice(sellPrice, buyPrice); + } + + 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); + } + + public record OrderPrice(double sell, double buy){} +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java new file mode 100644 index 00000000..3f43eacf --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java @@ -0,0 +1,54 @@ +package com.cleanengine.coin.realitybot.vo; + +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@NoArgsConstructor +public class OrderVolumePolicy { + + /** + * 평균 거래량과 추세 편차율을 기반으로 랜덤 거래량을 생성합니다. + * + * @param avgVolume 평균 거래량 + * @param trendLineRate platformVWAP - apiVWAP 편차율 (e.g., 0.03 = +3%) + * @param isBuy 매수면 true, 매도면 false + * @return 생성된 거래량 + */ + + public double calculateVolume(double avgVolume, double trendLineRate, boolean isBuy){ + //기본 랜덤 거래량 (0.5~1.5) + double rawVolume = avgVolume *(0.5*Math.random()); + + //편차에 따른 거래량 보정 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; //강한 추세 -> 강한 보정 + } + + //매수-매도 비중 조정 + if (deviation <=0.001) //0.1%일 경우 안정권 , 추가적인 보정 x + return volumeExpansion(rawVolume); + if (trendLineRate > 0){ + //시장이 상승하면 매도 강세보정 + return isBuy? volumeExpansion(rawVolume* 0.7) //소극적 매수 + : volumeExpansion(rawVolume*1.5); //적극적 매도 + } else { + //시장이 하락하면 매수 강세보정 + return isBuy? volumeExpansion(rawVolume*1.5) //적극적 매도 + : volumeExpansion(rawVolume*0.7); //소극적 매수 + } + } + + private double volumeExpansion(double rawVolume){ + double resultVolume = Math.round(rawVolume * 10000.0)/10000.0; + if(resultVolume <= 0) { + //Volume이 0이하일 경우 재 계산 + resultVolume = Math.round(rawVolume * 10000000.0) / 10000000.0; + } + return resultVolume; + } +} From d36a61baa242a46ab0e7a05188ea1b69ab202659 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Wed, 4 Jun 2025 04:21:42 +0900 Subject: [PATCH 18/31] =?UTF-8?q?test:=20=EA=B1=B0=EB=9E=98=EC=86=8C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B3=B4=EA=B0=84=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../realitybot/api/BithumbAPIClientTest.java | 5 +- .../parser/OpeningPriceParserTest.java | 31 ++++++++++ .../realitybot/parser/TickParserTest.java | 36 +++++++++++ .../VWAPerrorInJectionSchedulerTest.java | 59 +++++++++++++++++++ .../realitybot/vo/OrderPricePolicyTest.java | 43 ++++++++++++++ 5 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParserTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/parser/TickParserTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java create mode 100644 src/test/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicyTest.java diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java index 6e46bdef..7dac6fdc 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/BithumbAPIClientTest.java @@ -23,9 +23,6 @@ public class BithumbAPIClientTest { @InjectMocks BithumbAPIClient bithumbAPIClient; - @Test - void get() { - } private String ticker = "BTC"; private String tradeJson = " {\n" + " \"market\": \"KRW-BTC\",\n" + @@ -77,7 +74,7 @@ void callTradePrice() throws IOException { Response mockresponse = new Response.Builder() .request(mockrequest) .protocol(Protocol.HTTP_1_1) - .code(400) + .code(200) .message("OK") .body(responseBody) .build(); diff --git a/src/test/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParserTest.java b/src/test/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParserTest.java new file mode 100644 index 00000000..d8d89844 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/parser/OpeningPriceParserTest.java @@ -0,0 +1,31 @@ +package com.cleanengine.coin.realitybot.parser; + +import com.cleanengine.coin.realitybot.dto.OpeningPrice; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class OpeningPriceParserTest { + private OpeningPriceParser openingPriceParser; + + @BeforeEach + void setUp() { + openingPriceParser = new OpeningPriceParser(); + } + + @Test + @DisplayName("json이 주어지면 openingprice객체로 반환한다.") + void testParseGson(){ + //given + String json = "[{\"market\":\"BTC\", \"opening_price\":10000.0,\"trade_price\":15000.0}]"; + + //when + OpeningPrice openingPrice = openingPriceParser.parseGson(json); + + //then + assertEquals("BTC", openingPrice.getMarket()); + assertEquals(10000.0,openingPrice.getOpening_price()); + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/parser/TickParserTest.java b/src/test/java/com/cleanengine/coin/realitybot/parser/TickParserTest.java new file mode 100644 index 00000000..4cdd18f4 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/parser/TickParserTest.java @@ -0,0 +1,36 @@ +package com.cleanengine.coin.realitybot.parser; + +import com.cleanengine.coin.realitybot.dto.Ticks; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class TickParserTest { + private TickParser tickParser; + + @BeforeEach + void setUp() { + tickParser = new TickParser(); + } + + @Test + @DisplayName("json을 주면 ticks 객체를 반환한다.") + void parse() { + //given + String json = "[{\"market\":\"BTC\",\"trade_date_utc\":\"2025-06-03\",\"trade_time_utc\":\"10:00:00\",\"timestamp\":\"2025-06-03T10:00:00.000Z\",\"trade_price\":45000.0,\"trade_volume\":0.5,\"prev_closing_price\":44000.0,\"change_price\":1000.0,\"ask_bid\":\"ASK\",\"sequential_id\":123456}]"; + //when + List ticks = tickParser.parseGson(json); + //then + assertEquals(1, ticks.size()); + Ticks tick = ticks.get(0); + assertEquals("BTC", tick.getMarket()); + assertEquals(45000.0, tick.getTrade_price()); + assertEquals(0.5, tick.getTrade_volume()); + + } + +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java new file mode 100644 index 00000000..2432d626 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java @@ -0,0 +1,59 @@ +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 + VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler; + + @Test + @DisplayName("enableInjection() 호출 전에는 작동 안한다") + void doNotingInjection(){ + vwaPerrorInJectionScheduler.injectFakeTrade(); + verify(tradeRepository,never()).save(any()); + } + + @Test + @DisplayName("호출 후에 fateTrade 삽입") + void injectOnceAfterEnable(){ + vwaPerrorInJectionScheduler.enableInjection(); + vwaPerrorInJectionScheduler.injectFakeTrade(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Trade.class); + verify(tradeRepository,times(1)).save(captor.capture()); + + 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 diff --git a/src/test/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicyTest.java b/src/test/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicyTest.java new file mode 100644 index 00000000..1ef2fcc4 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicyTest.java @@ -0,0 +1,43 @@ +package com.cleanengine.coin.realitybot.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class OrderPricePolicyTest { + + + private final OrderPricePolicy policy = new OrderPricePolicy(); + + @Test + @DisplayName("Level 1: 매수는 VWAP 이상, 매도는 VWAP 이하이어야 함") + void testLevel1() { + double platformVWAP = 10000.0; + double unitPrice = 10.0; + double trendLineRate = 0.03; + + OrderPricePolicy.OrderPrice price = policy.calculatePrice(1, platformVWAP, unitPrice, trendLineRate); + + assertTrue(price.buy() > platformVWAP, "Level 1: 매수 가격은 VWAP보다 커야 함"); + assertTrue(price.sell() < platformVWAP, "Level 1: 매도 가격은 VWAP보다 작아야 함"); + assertEquals(0, price.buy() % unitPrice, "호가 단위로 정규화되어야 함"); + assertEquals(0, price.sell() % unitPrice, "호가 단위로 정규화되어야 함"); + } + + @Test + @DisplayName("Level 5: 가격 차이가 충분히 커야 함") + void testLevel5Spread() { + int level = 5; + double platformVWAP = 30000.0; + double unitPrice = 100.0; + double trendLineRate = 0.0; + + OrderPricePolicy.OrderPrice price = policy.calculatePrice(level, platformVWAP, unitPrice, trendLineRate); + + double minExpectedDiff = unitPrice * 5 * 0.8; // 랜덤 보정 고려해도 80% 이상 차이 기대 + assertTrue(price.sell() - price.buy() >= minExpectedDiff, + "레벨 5는 충분한 가격 차이를 가져야 함"); + } +} \ No newline at end of file From fdd6d6ea982727c7dc055caa81daa1a6b2e03f4f Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Wed, 4 Jun 2025 04:23:47 +0900 Subject: [PATCH 19/31] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B7=9C=20=EA=B1=B0?= =?UTF-8?q?=EB=9E=98=EC=86=8C=20=EC=B6=94=EA=B0=80=20-=20=EC=BD=94?= =?UTF-8?q?=EC=9D=B8=EC=9B=90=20=EC=B6=94=EA=B0=80=20issue:=20#39?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/ApiScheduler.java | 21 ++++++- .../coin/realitybot/api/BithumbAPIClient.java | 8 +++ .../coin/realitybot/api/CoinoneAPIClient.java | 60 +++++++++++++++++++ .../realitybot/dto/CoinoneTicksResponse.java | 29 +++++++++ .../parser/CoinoneTicksAdapter.java | 23 +++++++ .../coin/realitybot/parser/TickParser.java | 7 +++ 6 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/cleanengine/coin/realitybot/api/CoinoneAPIClient.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/dto/CoinoneTicksResponse.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/parser/CoinoneTicksAdapter.java 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 cf426b3c..0f482f3f 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -28,6 +28,7 @@ public class ApiScheduler { private final TickServiceManager tickServiceManager; private final Map lastSequentialIdMap = new ConcurrentHashMap<>(); private final AssetRepository assetRepository; + private final CoinoneAPIClient coinoneAPIClient; private String ticker; // @Scheduled(fixedRate = 5000) @@ -42,7 +43,8 @@ public void MarketAllRequest() throws InterruptedException { public void MarketDataRequest(String ticker){ this.ticker = ticker; - String rawJson = bithumbAPIClient.get(ticker); //api raw데이터 +// String rawJson = bithumbAPIClient.get(ticker); //api raw데이터 + String rawJson = getMarketDataWithFallback(ticker); List gson = tickParser.parseGson(rawJson); //json을 list로 변환 APIVWAPState apiVWAPState = tickServiceManager.getService(ticker); @@ -72,6 +74,23 @@ public void destroy() throws Exception { //담긴 Queue데이터 확인용 // orderQueueManagerService.logAllOrders(); // virtualTradeService.printOrderSummary(); }*/ +public String getMarketDataWithFallback(String ticker) { + try { +// String bithumbJson = bithumbAPIClient.get(ticker); + String bithumbJson = null; + // 예외가 없었어도 비정상 응답일 수 있음 → 예: 빈 JSON 또는 에러 코드 + if (bithumbJson == null || bithumbJson.isBlank() || bithumbJson.contains("\"result\":\"error\"")) { + log.warn("Bithumb 응답 비정상, Coinone으로 대체 요청"); + return coinoneAPIClient.get(ticker); + } + + return bithumbJson; + + } catch (Exception e) { + log.error("Bithumb API 오류 발생: {} → Coinone으로 대체 요청", e.getMessage()); + return coinoneAPIClient.get(ticker); + } +} } diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index 207a6dc3..39aea475 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -22,6 +22,14 @@ public String get(String ticker){ //API를 responseBody에 담아 반환 this.ticker = ticker; // client = new OkHttpClient(); // gson = new Gson(); + +// try { +// Thread.sleep(2500); +// } catch (InterruptedException e) { +// Thread.currentThread().interrupt(); +// } + + Request request = new Request.Builder() .url("https://api.bithumb.com/v1/trades/ticks?market=krw-"+ticker+"&count=10") .get() diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/CoinoneAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/CoinoneAPIClient.java new file mode 100644 index 00000000..29cf255a --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/api/CoinoneAPIClient.java @@ -0,0 +1,60 @@ +package com.cleanengine.coin.realitybot.api; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CoinoneAPIClient { + private final OkHttpClient client; + private String ticker; + + + public String get(String ticker){ //API를 responseBody에 담아 반환 + this.ticker = ticker; + Request request = new Request.Builder() + .url("https://api.coinone.co.kr/public/v2/trades/KRW/"+ticker+"?size=10") + .get() + .addHeader("accept", "application/json") + .build(); + try (Response response = client.newCall(request).execute()){ + if ((response.code() == 400)){ + log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker); + } + String responseBody = response.body().string(); +// return gson.toJson(response.body().string()); + log.info("{}의 Bithumb API 응답 : {}",ticker,responseBody); + return responseBody; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +/* public String getOpeningPrice(String ticker){ + this.ticker = ticker; + Request request = new Request.Builder() + .url("https://api.bithumb.com/v1/ticker?markets=KRW-"+ticker) + .get() + .addHeader("accept", "application/json") + .build(); + try (Response response = client.newCall(request).execute()){ + if ((response.code() == 400)){ + log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker); + } + String responseBody = response.body().string(); +// return gson.toJson(response.body().string()); + log.debug("{}의 OpeningPirce 응답 : {}",ticker,responseBody); + return responseBody; + } catch (IOException e) { + throw new RuntimeException("API 요청 중 예외 발생",e); + } + }*/ + +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/dto/CoinoneTicksResponse.java b/src/main/java/com/cleanengine/coin/realitybot/dto/CoinoneTicksResponse.java new file mode 100644 index 00000000..aa345a53 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/dto/CoinoneTicksResponse.java @@ -0,0 +1,29 @@ +package com.cleanengine.coin.realitybot.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class CoinoneTicksResponse { + private String result; + private String error_code; + private long server_time; + private String quote_currency; + private String target_currency; + private List trades; + + @Getter + @Setter + public static class Trades { + private String id; + private long timestamp; + private String price; + private String qty; + private boolean is_seller_maker; + } +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/parser/CoinoneTicksAdapter.java b/src/main/java/com/cleanengine/coin/realitybot/parser/CoinoneTicksAdapter.java new file mode 100644 index 00000000..47598a77 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/parser/CoinoneTicksAdapter.java @@ -0,0 +1,23 @@ +package com.cleanengine.coin.realitybot.parser; + +import com.cleanengine.coin.realitybot.dto.CoinoneTicksResponse; +import com.cleanengine.coin.realitybot.dto.Ticks; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class CoinoneTicksAdapter { + public List convertToTicks(CoinoneTicksResponse response, String market){ + return response.getTrades().stream() + .map(tx -> Ticks.builder() + .market(market) + .timestamp(String.valueOf(tx.getTimestamp())) + .trade_price(Float.parseFloat(tx.getPrice())) + .trade_volume(Double.parseDouble(tx.getQty())) + .ask_bid(tx.is_seller_maker() ? "ASK" : "BID") + .sequential_id(Long.parseLong(tx.getId())) + .build()) + .toList(); + } +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java b/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java index be643cc8..3ca4bbb8 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java +++ b/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java @@ -1,5 +1,6 @@ package com.cleanengine.coin.realitybot.parser; +import com.cleanengine.coin.realitybot.dto.CoinoneTicksResponse; import com.cleanengine.coin.realitybot.dto.Ticks; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -16,8 +17,14 @@ @Getter public class TickParser { private final Gson gson = new Gson(); + private final CoinoneTicksAdapter coinoneAdapter; + private final TickParser tickParser; public List parseGson(String json) { + if (exchange.equalsIgnoreCase("coinone") || json.contains("transactions")) { + CoinoneTicksResponse response = gson.fromJson(json, CoinoneTicksResponse.class); + return coinoneAdapter.convertToTicks(response, "KRW-" + ticker.toUpperCase()); + } else return gson.fromJson(json, new TypeToken>() {}.getType()); } /* From c8aecc1a0c793ff062e8944d01e87f98de90c45b Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Wed, 4 Jun 2025 04:24:02 +0900 Subject: [PATCH 20/31] =?UTF-8?q?test:=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/RealitybotCoreTestSuite.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java index b0ab038b..196ead0d 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java +++ b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java @@ -11,8 +11,11 @@ import com.cleanengine.coin.realitybot.domain.VWAPCalculatorTest; import com.cleanengine.coin.realitybot.dto.OpeningPriceTest; import com.cleanengine.coin.realitybot.dto.TicksTest; +import com.cleanengine.coin.realitybot.parser.OpeningPriceParserTest; +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.UnitPricePolicyTest; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @@ -32,7 +35,10 @@ TickServiceManagerTest.class, VWAPCalculatorTest.class, APIVWAPStateTest.class, - TicksTest.class + TicksTest.class, + TickParserTest.class, + OpeningPriceParserTest.class, + VWAPerrorInJectionSchedulerTest.class, }) public class RealitybotCoreTestSuite { } From b504a5029d4559ecd647d7f4e5e8d2555c6f9a98 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Thu, 5 Jun 2025 08:10:35 +0900 Subject: [PATCH 21/31] =?UTF-8?q?refactor:=20=EA=B0=80=EA=B2=A9=20?= =?UTF-8?q?=EC=A0=95=EC=B1=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/ApiScheduler.java | 8 ++++---- .../coin/realitybot/parser/TickParser.java | 12 +++++------ .../realitybot/vo/DeviationPricePolicy.java | 20 ++++++++++++++----- .../coin/realitybot/vo/OrderPricePolicy.java | 4 ++-- 4 files changed, 27 insertions(+), 17 deletions(-) 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 0f482f3f..43e412c0 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -43,8 +43,8 @@ public void MarketAllRequest() throws InterruptedException { public void MarketDataRequest(String ticker){ this.ticker = ticker; -// String rawJson = bithumbAPIClient.get(ticker); //api raw데이터 - String rawJson = getMarketDataWithFallback(ticker); + String rawJson = bithumbAPIClient.get(ticker); //api raw데이터 +// String rawJson = getMarketDataWithFallback(ticker); List gson = tickParser.parseGson(rawJson); //json을 list로 변환 APIVWAPState apiVWAPState = tickServiceManager.getService(ticker); @@ -74,7 +74,7 @@ public void destroy() throws Exception { //담긴 Queue데이터 확인용 // orderQueueManagerService.logAllOrders(); // virtualTradeService.printOrderSummary(); }*/ -public String getMarketDataWithFallback(String ticker) { +/*public String getMarketDataWithFallback(String ticker) { try { // String bithumbJson = bithumbAPIClient.get(ticker); String bithumbJson = null; @@ -91,6 +91,6 @@ public String getMarketDataWithFallback(String ticker) { log.error("Bithumb API 오류 발생: {} → Coinone으로 대체 요청", e.getMessage()); return coinoneAPIClient.get(ticker); } -} +}*/ } diff --git a/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java b/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java index 3ca4bbb8..30289ea9 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java +++ b/src/main/java/com/cleanengine/coin/realitybot/parser/TickParser.java @@ -17,14 +17,14 @@ @Getter public class TickParser { private final Gson gson = new Gson(); - private final CoinoneTicksAdapter coinoneAdapter; - private final TickParser tickParser; +// private final CoinoneTicksAdapter coinoneAdapter; +// private final TickParser tickParser; public List parseGson(String json) { - if (exchange.equalsIgnoreCase("coinone") || json.contains("transactions")) { - CoinoneTicksResponse response = gson.fromJson(json, CoinoneTicksResponse.class); - return coinoneAdapter.convertToTicks(response, "KRW-" + ticker.toUpperCase()); - } else +// if (exchange.equalsIgnoreCase("coinone") || json.contains("transactions")) { +// CoinoneTicksResponse response = gson.fromJson(json, CoinoneTicksResponse.class); +// return coinoneAdapter.convertToTicks(response, "KRW-" + ticker.toUpperCase()); +// } else return gson.fromJson(json, new TypeToken>() {}.getType()); } /* 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 89e6f310..523015a0 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java @@ -25,12 +25,22 @@ public AdjustPrice adjust(double platformSell,double platformBuy, double trendLi double weight = getCorrectionWeight(deviation); double closeness = 0.5 + (weight * 0.3); // 보간 가중치: 0.7 ~ 1.0 -> 0.5 - double targetVWAP = (trendLineRate > 0) //만약 closeness 를 0.5 입력시 중간값 - ? apiVWAP + (platformSell - apiVWAP) * closeness // 고평가 → platformSell(25000) → apiVWAP(16000) 사이 가중치 %로 유도 - : apiVWAP - (apiVWAP - platformBuy) * closeness; // 저평가 → platformBuy(12000) ← apiVWAP(16000) 사이 가중치 %로 유도 +// double targetVWAP = (trendLineRate > 0) //만약 closeness 를 0.5 입력시 중간값 +// ? apiVWAP + (platformSell - apiVWAP) * closeness // 고평가 → platformSell(25000) → apiVWAP(16000) 사이 가중치 %로 유도 +// : apiVWAP - (apiVWAP - platformBuy) * closeness; // 저평가 → platformBuy(12000) ← apiVWAP(16000) 사이 가중치 %로 유도 + double sellTarget, buyTarget; + if (trendLineRate > 0) { + // sell은 platformSell에서 apiVWAP 쪽으로 낮춤 + sellTarget = apiVWAP + (platformSell - apiVWAP) * closeness; + buyTarget = apiVWAP + (platformBuy - apiVWAP) * closeness; + } else { + // 저평가일 경우: 가격을 올림 + sellTarget = apiVWAP - (apiVWAP - platformSell) * closeness; + buyTarget = apiVWAP - (apiVWAP - platformBuy) * closeness; + } - double adjustedSell = normalizeToUnit(interpolate(platformSell,targetVWAP ,weight),unitPrice); - double adjustedBuy = normalizeToUnit(interpolate(platformBuy,targetVWAP ,weight),unitPrice); + double adjustedSell = normalizeToUnit(interpolate(platformSell,sellTarget ,weight),unitPrice); + double adjustedBuy = normalizeToUnit(interpolate(platformBuy,buyTarget ,weight),unitPrice); return new AdjustPrice(adjustedSell,adjustedBuy); 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 fc185ef2..fefa8582 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicy.java @@ -19,17 +19,17 @@ public OrderPrice calculatePrice(int level, double priceOffset = unitPrice * level; double sellPrice, buyPrice; double randomOffset = Math.abs(getRandomOffset(platformVWAP,getDynamicMaxRate(trendLineRate))); + double basePrice = normalizeToUnit(platformVWAP, unitPrice); //기준 가격 (호가 단위 정규화) if (level == 1){ //1level일 경우 주문이 겹치도록 설정 - double basePrice = normalizeToUnit(platformVWAP, unitPrice); //기준 가격 (호가 단위 정규화) //체결을 위해 매수가 올리고, 매도가 내리는 계산 적용 sellPrice = normalizeToUnit(basePrice - randomOffset,unitPrice); buyPrice = normalizeToUnit(basePrice + randomOffset,unitPrice); } //2~3 단계 : orderbook 단위 주문 else { - randomOffset = getRandomOffset(platformVWAP,0.01); + randomOffset = getRandomOffset(platformVWAP,0.001); //체결 확률 증가용 코드 sellPrice = normalizeToUnit(platformVWAP + priceOffset - randomOffset,unitPrice); buyPrice = normalizeToUnit(platformVWAP - priceOffset + randomOffset,unitPrice); From ebe6e44796e4d7a6fffc7c01e670baf34c4831e6 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Thu, 5 Jun 2025 08:10:50 +0900 Subject: [PATCH 22/31] =?UTF-8?q?test:=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../realitybot/RealitybotCoreTestSuite.java | 4 ++ .../realitybot/api/RefresherRunnerTest.java | 1 + .../vo/DeviationPricePolicyTest.java | 39 +++++++++++++++++++ .../realitybot/vo/OrderPricePolicyTest.java | 25 ++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 src/test/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicyTest.java diff --git a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java index 196ead0d..74bad746 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java +++ b/src/test/java/com/cleanengine/coin/realitybot/RealitybotCoreTestSuite.java @@ -16,6 +16,8 @@ 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; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @@ -39,6 +41,8 @@ TickParserTest.class, OpeningPriceParserTest.class, VWAPerrorInJectionSchedulerTest.class, + OrderPricePolicyTest.class, + DeviationPricePolicyTest.class }) public class RealitybotCoreTestSuite { } 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 be5b4ce5..cfb7bf79 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java @@ -1,5 +1,6 @@ package com.cleanengine.coin.realitybot.api; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.ApplicationArguments; diff --git a/src/test/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicyTest.java b/src/test/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicyTest.java new file mode 100644 index 00000000..43407525 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicyTest.java @@ -0,0 +1,39 @@ +package com.cleanengine.coin.realitybot.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest +public class DeviationPricePolicyTest { + private final DeviationPricePolicy policy = new DeviationPricePolicy(); + + @Test + @DisplayName("1% 이하 편차라면 보정안한다.") + void noAjustWhenDeviationLessThan1(){ + DeviationPricePolicy.AdjustPrice result = policy.adjust(15000,14000, 0,14500,100); + + assertEquals(15000,result.sell()); + assertEquals(14000,result.buy()); + } + + @Test + @DisplayName("시장이 고평가일 때 보정한다.") + void adjustWhenOverValue(){ + var result = policy.adjust(25000,24000, 0.05,19000,1000); + System.out.println(result.sell()); + System.out.println(result.buy()); + assertTrue(result.sell() < 25000); + assertTrue(result.buy() < 24000); + } + @Test + @DisplayName("시장이 저평가일 때 보정한다.") + void adjustWhenUnderValue(){ + var result = policy.adjust(15000,14000, -0.05,19000,1000); + System.out.println(result.sell()); + System.out.println(result.buy()); + assertTrue(result.sell() > 15000); + assertTrue(result.buy() > 14000); + } +} \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicyTest.java b/src/test/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicyTest.java index 1ef2fcc4..2658a975 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicyTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/vo/OrderPricePolicyTest.java @@ -40,4 +40,29 @@ void testLevel5Spread() { assertTrue(price.sell() - price.buy() >= minExpectedDiff, "레벨 5는 충분한 가격 차이를 가져야 함"); } + + @RepeatedTest(5) + @DisplayName("Level 2~3: 가격 차이는 허용 범위 내, 호가 단위로 정규화됨") + void testLevel2To3_priceDiffWithinRange() { + for (int level = 2; level <= 3; level++) { + double platformVWAP = 20000.0; + double unitPrice = 50.0; + double trendLineRate = -0.02; + + OrderPricePolicy.OrderPrice price = policy.calculatePrice(level, platformVWAP, unitPrice, trendLineRate); + + double priceDiff = price.sell() - price.buy(); + double priceOffset = unitPrice * level; + double maxRandomOffset = platformVWAP * 0.01; + double maxAllowedDiff = (priceOffset + maxRandomOffset) * 2; + + System.out.printf("level=%d, sell=%.1f, buy=%.1f, diff=%.1f, maxAllowed=%.1f%n", + level, price.sell(), price.buy(), priceDiff, maxAllowedDiff); + + assertTrue(Math.abs(priceDiff) <= maxAllowedDiff, + String.format("가격 차이 %.1f 이 최대 허용 범위 %.1f 초과", priceDiff, maxAllowedDiff)); + assertEquals(0, price.sell() % unitPrice, "매도 가격은 호가 단위여야 함"); + assertEquals(0, price.buy() % unitPrice, "매수 가격은 호가 단위여야 함"); + } + } } \ No newline at end of file From 5aa19ff5c3321e1f7b39de1a294e92da5432036c Mon Sep 17 00:00:00 2001 From: Junh-b Date: Thu, 5 Jun 2025 10:49:36 +0900 Subject: [PATCH 23/31] =?UTF-8?q?fix:=20=EC=A3=BC=EB=AC=B8=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=9E=84=EC=8B=9C=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 반환 예외 타입 변경으로 인한 핸들링 로직 변경 --- .../coin/realitybot/service/OrderGenerateService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 849d769b..11d23596 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -1,9 +1,9 @@ package com.cleanengine.coin.realitybot.service; import com.cleanengine.coin.common.error.DomainValidationException; -import com.cleanengine.coin.order.application.OrderService; 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.trade.entity.Trade; import com.cleanengine.coin.trade.repository.TradeRepository; import com.cleanengine.coin.user.domain.Account; @@ -170,7 +170,7 @@ private void createOrderWithFallback(String ticker,boolean isBuy, double volume, try { orderService.createOrderWithBot(ticker, isBuy, volume, price); - } catch (DomainValidationException e) { + } catch (IllegalArgumentException e) { log.debug("잔량 부족: {}", e.getMessage()); try { resetBot(ticker); From 0635421853c968d508af6768e6ee80fbff7534a7 Mon Sep 17 00:00:00 2001 From: Junh-b Date: Thu, 5 Jun 2025 10:51:55 +0900 Subject: [PATCH 24/31] =?UTF-8?q?fix:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EB=B3=84=EB=8F=84=20=EC=8A=A4?= =?UTF-8?q?=EB=A0=88=EB=93=9C=20=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 최근 반영사항으로 일시적으로 기존 싱글 스레드에서 처리되던 로직을 다시 별도 스레드 동작 방식으로 변경했습니다. --- src/main/java/com/cleanengine/coin/CoinApplication.java | 2 ++ .../coin/trade/application/TradeQueueManager.java | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/CoinApplication.java b/src/main/java/com/cleanengine/coin/CoinApplication.java index 21ed9457..cf0d1fe1 100644 --- a/src/main/java/com/cleanengine/coin/CoinApplication.java +++ b/src/main/java/com/cleanengine/coin/CoinApplication.java @@ -3,12 +3,14 @@ import jakarta.annotation.PostConstruct; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import java.util.TimeZone; @EnableScheduling +@EnableAsync @SpringBootApplication public class CoinApplication { diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java b/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java index 6dab0368..0f81de83 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java @@ -1,7 +1,10 @@ package com.cleanengine.coin.trade.application; import com.cleanengine.coin.order.application.event.OrderCreated; +import com.cleanengine.coin.order.application.event.OrderInsertedToQueue; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; @@ -15,8 +18,8 @@ public TradeQueueManager(TradeFlowService tradeFlowService) { this.tradeFlowService = tradeFlowService; } - @TransactionalEventListener - public void handleOrderInserted(OrderCreated event) { + @EventListener @Async + public void handleOrderInserted(OrderInsertedToQueue event) { try { tradeFlowService.execMatchAndTrade(event.order().getTicker()); } catch (Exception e) { From 04e121ff70e063fa7e446478e3abb8ca25200abf Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Thu, 5 Jun 2025 13:56:19 +0900 Subject: [PATCH 25/31] =?UTF-8?q?refactor:=20exception=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cleanengine/coin/realitybot/api/ApiScheduler.java | 2 +- .../coin/realitybot/service/OrderGenerateService.java | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) 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 43e412c0..df7eb89b 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -3,9 +3,9 @@ import com.cleanengine.coin.common.annotation.WorkingServerProfile; import com.cleanengine.coin.order.domain.Asset; import com.cleanengine.coin.order.infra.AssetRepository; +import com.cleanengine.coin.realitybot.domain.APIVWAPState; import com.cleanengine.coin.realitybot.dto.Ticks; import com.cleanengine.coin.realitybot.parser.TickParser; -import com.cleanengine.coin.realitybot.domain.APIVWAPState; import com.cleanengine.coin.realitybot.service.OrderGenerateService; import com.cleanengine.coin.realitybot.service.TickServiceManager; import lombok.RequiredArgsConstructor; 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 a5046751..f62e7c65 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -1,6 +1,5 @@ package com.cleanengine.coin.realitybot.service; -import com.cleanengine.coin.common.error.DomainValidationException; import com.cleanengine.coin.order.application.OrderService; import com.cleanengine.coin.order.external.adapter.account.AccountExternalRepository; import com.cleanengine.coin.order.external.adapter.wallet.WalletExternalRepository; @@ -103,17 +102,17 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// ); } - private void createOrderWithFallback(String ticker,boolean isBuy, double volume, double price ) { + private void createOrderWithFallback(String ticker,boolean isBuy, double volume, double price ) throws IllegalArgumentException { if (volume <= 0 || price <= 0){ log.error("잘못된 주문이 발생 [종목 : {}] ,[isBuy : {}] ,[금액 : {}] ,[수량 : {}] 주문은 생성 취소",ticker,isBuy, new DecimalFormat("#,###.########").format(price), new DecimalFormat("#,###.########").format(volume)); return; - } - + } + try { orderService.createOrderWithBot(ticker, isBuy, volume, price); - } catch (DomainValidationException e) { + } catch (IllegalArgumentException e) { log.debug("잔량 부족: {}", e.getMessage()); try { resetBot(ticker); From a3e8a62e211e067effd05ddb9b5715a24f7de9d5 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Thu, 5 Jun 2025 14:26:27 +0900 Subject: [PATCH 26/31] =?UTF-8?q?refactor:=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 49074ec9..db3f2c8f 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java @@ -1,7 +1,7 @@ package com.cleanengine.coin.realitybot.api; +import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; import com.cleanengine.coin.order.domain.Asset; -import com.cleanengine.coin.order.infra.AssetRepository; import com.cleanengine.coin.realitybot.dto.OpeningPrice; import com.cleanengine.coin.realitybot.parser.OpeningPriceParser; import com.cleanengine.coin.realitybot.vo.UnitPricePolicy; From 1d9a8ba5e38bdc2e8522d78248590b3029c2e47c Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Thu, 5 Jun 2025 15:39:06 +0900 Subject: [PATCH 27/31] =?UTF-8?q?refactor:=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 6 +++++- .../cleanengine/coin/realitybot/api/ApiSchedulerTest.java | 4 ++-- .../coin/realitybot/api/UnitPriceRefresherTest.java | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 2fbd62f9..6ec6fe43 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -26,4 +26,8 @@ spring: secure: false frontend: - url: "http://localhost:5173/callback" \ No newline at end of file + 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/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java index 31dedd92..1eaca43f 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/ApiSchedulerTest.java @@ -1,10 +1,10 @@ package com.cleanengine.coin.realitybot.api; +import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; import com.cleanengine.coin.order.domain.Asset; -import com.cleanengine.coin.order.infra.AssetRepository; +import com.cleanengine.coin.realitybot.domain.APIVWAPState; import com.cleanengine.coin.realitybot.dto.Ticks; import com.cleanengine.coin.realitybot.parser.TickParser; -import com.cleanengine.coin.realitybot.domain.APIVWAPState; import com.cleanengine.coin.realitybot.service.OrderGenerateService; import com.cleanengine.coin.realitybot.service.TickServiceManager; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java index f0aa2532..38fcb970 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresherTest.java @@ -1,7 +1,7 @@ package com.cleanengine.coin.realitybot.api; +import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; import com.cleanengine.coin.order.domain.Asset; -import com.cleanengine.coin.order.infra.AssetRepository; import com.cleanengine.coin.realitybot.dto.OpeningPrice; import com.cleanengine.coin.realitybot.parser.OpeningPriceParser; import com.cleanengine.coin.realitybot.vo.UnitPricePolicy; From 60cabd844ba51352f0b6d25ee6024280c7df5c2f Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Thu, 5 Jun 2025 15:41:26 +0900 Subject: [PATCH 28/31] =?UTF-8?q?refactor:=20log=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cleanengine/coin/realitybot/api/BithumbAPIClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java index 39aea475..653a08c5 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java @@ -37,7 +37,7 @@ public String get(String ticker){ //API를 responseBody에 담아 반환 .build(); try (Response response = client.newCall(request).execute()){ if ((response.code() == 400)){ - log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker); + log.warn("DB asset 최신화가 필요합니다 : {}",ticker); } String responseBody = response.body().string(); // return gson.toJson(response.body().string()); @@ -56,7 +56,7 @@ public String getOpeningPrice(String ticker){ .build(); try (Response response = client.newCall(request).execute()){ if ((response.code() == 400)){ - log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker); + log.warn("DB asset 최신화가 필요합니다 : {}",ticker); } String responseBody = response.body().string(); // return gson.toJson(response.body().string()); From 0871699baf26bc84300771645e785d5b47faee85 Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 14:19:22 +0900 Subject: [PATCH 29/31] =?UTF-8?q?refactor:=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=EC=9E=90=20=EB=B0=8F=20=EB=A6=AC=ED=84=B4=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/trade/application/TradeExecutor.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java index b59e7c9f..1e96fbfa 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java @@ -2,7 +2,6 @@ import com.cleanengine.coin.common.error.BusinessException; import com.cleanengine.coin.common.response.ErrorStatus; -import com.cleanengine.coin.order.application.OrderService; import com.cleanengine.coin.order.domain.BuyOrder; import com.cleanengine.coin.order.domain.Order; import com.cleanengine.coin.order.domain.OrderStatus; @@ -89,12 +88,12 @@ public void executeTrade(WaitingOrders waitingOrders, TradePair tr tradeExecutedEventPublisher.publish(tradeExecutedEvent); } - public void increaseAccountCash(Order order, Double amount) { + private Account increaseAccountCash(Order order, Double amount) { Account account = accountService.findAccountByUserId(order.getUserId()).orElseThrow(); - accountService.save(account.increaseCash(amount)); + return accountService.save(account.increaseCash(amount)); } - public void updateWalletAfterTrade(Order order, String ticker, double tradedSize, double totalTradedPrice) { + private Wallet updateWalletAfterTrade(Order order, String ticker, double tradedSize, double totalTradedPrice) { if (order instanceof BuyOrder) { Wallet buyerWallet = walletService.findWalletByUserIdAndTicker(order.getUserId(), ticker); double updatedBuySize = buyerWallet.getSize() + tradedSize; @@ -103,17 +102,17 @@ public void updateWalletAfterTrade(Order order, String ticker, double tradedSize buyerWallet.setSize(updatedBuySize); buyerWallet.setBuyPrice(updatedBuyPrice); // TODO : ROI 계산 - walletService.save(buyerWallet); + return walletService.save(buyerWallet); } else if (order instanceof SellOrder) { // 매도 시에는 평단가 변동 없음 Wallet sellerWallet = walletService.findWalletByUserIdAndTicker(order.getUserId(), ticker); - walletService.save(sellerWallet); + return walletService.save(sellerWallet); } else { throw new BusinessException("Unsupported order type: " + order.getClass().getName(), ErrorStatus.INTERNAL_SERVER_ERROR); } } - public Trade insertNewTrade(String ticker, BuyOrder buyOrder, SellOrder sellOrder, double tradeSize, Double tradePrice) { + private Trade insertNewTrade(String ticker, BuyOrder buyOrder, SellOrder sellOrder, double tradeSize, Double tradePrice) { Trade newTrade = Trade.of(ticker, LocalDateTime.now(), buyOrder.getUserId(), sellOrder.getUserId(), tradePrice, tradeSize); return tradeService.save(newTrade); @@ -184,7 +183,7 @@ private void removeCompletedSellOrder(WaitingOrders waitingOrders, SellOrder ord } } - public void updateCompletedOrderStatus(Order order) { + private void updateCompletedOrderStatus(Order order) { order.setState(OrderStatus.DONE); } From 9d4168741716460e94f198c6cd119408db694383 Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 14:20:54 +0900 Subject: [PATCH 30/31] =?UTF-8?q?fix:=20=EC=B2=B4=EA=B2=B0=20=EB=A9=80?= =?UTF-8?q?=ED=8B=B0=EC=8A=A4=EB=A0=88=EB=93=9C=20->=20=EB=8B=A8=EC=9D=BC?= =?UTF-8?q?=EC=8A=A4=EB=A0=88=EB=93=9C=20=EB=B3=80=EA=B2=BD(=EC=B2=B4?= =?UTF-8?q?=EA=B2=B0=20=EC=95=88=EC=A0=95=ED=99=94=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=9E=84=EC=8B=9C=20=EC=A1=B0=EC=B9=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cleanengine/coin/trade/application/TradeFlowService.java | 3 ++- .../cleanengine/coin/trade/application/TradeQueueManager.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeFlowService.java b/src/main/java/com/cleanengine/coin/trade/application/TradeFlowService.java index 30f2415e..5a7cab22 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeFlowService.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeFlowService.java @@ -5,19 +5,20 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Slf4j @RequiredArgsConstructor -@Transactional @Component public class TradeFlowService { private final TradeMatcher tradeMatcher; private final TradeExecutor tradeExecutor; + @Transactional(propagation = Propagation.REQUIRES_NEW) public void execMatchAndTrade(String ticker) { WaitingOrders waitingOrders = tradeMatcher.getWaitingOrders(ticker); // TODO : peek() 해온 Order 객체들을 lock -> 체결 도중 취소 방지 diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java b/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java index 0f81de83..46d84d26 100644 --- a/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java +++ b/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java @@ -18,7 +18,7 @@ public TradeQueueManager(TradeFlowService tradeFlowService) { this.tradeFlowService = tradeFlowService; } - @EventListener @Async + @EventListener public void handleOrderInserted(OrderInsertedToQueue event) { try { tradeFlowService.execMatchAndTrade(event.order().getTicker()); From 53ad317906d60f9e3b87258e323abd3715051547 Mon Sep 17 00:00:00 2001 From: caniro Date: Fri, 6 Jun 2025 14:21:38 +0900 Subject: [PATCH 31/31] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20Disable=20=EC=A7=80=EC=A0=95(?= =?UTF-8?q?=EC=B6=94=ED=9B=84=20=EC=9E=AC=EC=9E=91=EC=84=B1=20=ED=95=84?= =?UTF-8?q?=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/trade/application/TradeExecuteLoadTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/cleanengine/coin/trade/application/TradeExecuteLoadTest.java b/src/test/java/com/cleanengine/coin/trade/application/TradeExecuteLoadTest.java index dded4ca8..dd4aa933 100644 --- a/src/test/java/com/cleanengine/coin/trade/application/TradeExecuteLoadTest.java +++ b/src/test/java/com/cleanengine/coin/trade/application/TradeExecuteLoadTest.java @@ -3,7 +3,6 @@ import com.cleanengine.coin.common.domain.port.PriorityQueueStore; import com.cleanengine.coin.order.application.OrderService; import com.cleanengine.coin.order.application.dto.OrderCommand; -import com.cleanengine.coin.order.application.dto.OrderInfo; import com.cleanengine.coin.order.domain.BuyOrder; import com.cleanengine.coin.order.domain.OrderType; import com.cleanengine.coin.order.domain.SellOrder; @@ -11,6 +10,7 @@ import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager; import com.cleanengine.coin.trade.repository.TradeRepository; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +20,7 @@ import java.time.LocalDateTime; @SpringBootTest +@Disabled class TradeExecuteLoadTest { @Autowired