From 70cf64de5b62167c84c6a3f69a428cda7eed6ab3 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Fri, 6 Jun 2025 15:12:08 +0900 Subject: [PATCH 01/11] =?UTF-8?q?refactor:=20errorinjection=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20-=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/OrderGenerateService.java | 3 --- .../service/VWAPErrorInjector.java} | 23 ++++--------------- .../VWAPerrorInJectionSchedulerTest.java | 22 ++++-------------- 3 files changed, 8 insertions(+), 40 deletions(-) rename src/{main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java => test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java} (60%) diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java index 4d2c878a..c8c0bbb2 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -36,7 +36,6 @@ public class OrderGenerateService { private final PlatformVWAPService platformVWAPService; private final OrderService orderService; private final TradeRepository tradeRepository; - private final VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler; private final OrderPricePolicy orderPricePolicy; private final DeviationPricePolicy deviationPricePolicy; private final OrderVolumePolicy orderVolumePolicy; @@ -80,8 +79,6 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// } catch (InterruptedException e) { throw new RuntimeException(e); } -// vwaPerrorInJectionScheduler.enableInjection(); //에러 발생기 비활성화 - /* DecimalFormat df = new DecimalFormat("#,##0.00"); DecimalFormat dfv = new DecimalFormat("#,###.########"); //모니터링용 diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java similarity index 60% rename from src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java rename to src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java index 059e24d1..6806f901 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPErrorInjector.java @@ -2,8 +2,6 @@ import com.cleanengine.coin.trade.entity.Trade; import com.cleanengine.coin.trade.repository.TradeRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @@ -12,23 +10,14 @@ import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; @Component -@RequiredArgsConstructor -public class VWAPerrorInJectionScheduler { - +public class VWAPErrorInjector { private final TradeRepository tradeRepository; - private boolean shouldInject = false; - private boolean hasInjected = false; - - public void enableInjection() { - if (hasInjected) return; // 이미 실행한 적 있으면 무시 - this.shouldInject = true; + public VWAPErrorInjector(TradeRepository tradeRepository) { + this.tradeRepository = tradeRepository; } - @Scheduled(fixedRate = 30000) // 혹은 따로 수동 호출도 가능 - public void injectFakeTrade() { - if (!shouldInject || hasInjected) return; - + public void injectErrorTrade(){ Trade fakeTrade = new Trade(); fakeTrade.setTicker("TRUMP"); fakeTrade.setBuyUserId(BUY_ORDER_BOT_ID); // 테스트용 유저 ID @@ -40,9 +29,5 @@ public void injectFakeTrade() { fakeTrade.setTradeTime(LocalDateTime.now()); tradeRepository.save(fakeTrade); - hasInjected = true; // ✅ 한 번 실행했으니 플래그 설정 - shouldInject = false; - - System.out.println("🚨 혼동 Trade 1건 삽입 완료!"); } } diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java index 2432d626..a5c884a8 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionSchedulerTest.java @@ -21,20 +21,20 @@ public class VWAPerrorInJectionSchedulerTest { TradeRepository tradeRepository; @InjectMocks - VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler; + VWAPErrorInjector vwapErrorInjector; + @Test @DisplayName("enableInjection() 호출 전에는 작동 안한다") void doNotingInjection(){ - vwaPerrorInJectionScheduler.injectFakeTrade(); + vwapErrorInjector.injectErrorTrade(); verify(tradeRepository,never()).save(any()); } @Test @DisplayName("호출 후에 fateTrade 삽입") void injectOnceAfterEnable(){ - vwaPerrorInJectionScheduler.enableInjection(); - vwaPerrorInJectionScheduler.injectFakeTrade(); + vwapErrorInjector.injectErrorTrade(); ArgumentCaptor captor = ArgumentCaptor.forClass(Trade.class); verify(tradeRepository,times(1)).save(captor.capture()); @@ -42,18 +42,4 @@ void injectOnceAfterEnable(){ Trade trade = captor.getValue(); assertEquals("TRUMP",trade.getTicker()); } - @Test - @DisplayName("한 번 삽입 이후 재삽입 되지 않음") - void onlyOnecInject(){ - vwaPerrorInJectionScheduler.enableInjection(); - verify(tradeRepository, never()).save(any()); - vwaPerrorInJectionScheduler.injectFakeTrade(); - verify(tradeRepository,times(1)).save(any()); - - vwaPerrorInJectionScheduler.injectFakeTrade(); - vwaPerrorInJectionScheduler.enableInjection(); - verify(tradeRepository,times(1)).save(any()); - - } - } \ No newline at end of file From 220b8f9c02a02b81296920955d248628c19f1ff2 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Sat, 7 Jun 2025 04:47:36 +0900 Subject: [PATCH 02/11] =?UTF-8?q?config:=20prometheus,=20grafana=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../coin/realitybot/api/ApiScheduler.java | 19 +++++++++++++------ .../coin/realitybot/config/MetricConfig.java | 15 +++++++++++++++ src/main/resources/application-actuator.yml | 12 ++++++++++++ src/main/resources/application.yml | 4 ++-- 5 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java create mode 100644 src/main/resources/application-actuator.yml diff --git a/build.gradle b/build.gradle index ecfc5b0a..1e947b67 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' implementation 'org.springframework.boot:spring-boot-starter-actuator' - + implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.13.1' diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java index 8fa1e380..3aa66874 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -8,8 +8,11 @@ import com.cleanengine.coin.realitybot.parser.TickParser; import com.cleanengine.coin.realitybot.service.OrderGenerateService; import com.cleanengine.coin.realitybot.service.TickServiceManager; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; @@ -29,16 +32,20 @@ public class ApiScheduler { private final Map lastSequentialIdMap = new ConcurrentHashMap<>(); private final AssetRepository assetRepository; private final CoinoneAPIClient coinoneAPIClient; + private final MeterRegistry meterRegistry; private String ticker; -// @Scheduled(fixedRate = 5000) + @Scheduled(fixedRate = 5000) public void MarketAllRequest() throws InterruptedException { - List tickers = assetRepository.findAll(); - for (Asset ticker : tickers){ - String tickerName = ticker.getTicker(); - MarketDataRequest(tickerName); + Timer timer = meterRegistry.timer("apischeduler.request.duration"); + timer.record(() -> { + List tickers = assetRepository.findAll(); + for (Asset ticker : tickers) { + String tickerName = ticker.getTicker(); + MarketDataRequest(tickerName); // Thread.sleep(500); - } + } + }); } public void MarketDataRequest(String ticker){ diff --git a/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java b/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java new file mode 100644 index 00000000..a55b49c6 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java @@ -0,0 +1,15 @@ +package com.cleanengine.coin.realitybot.config; + +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MetricConfig { + + @Bean + MeterRegistryCustomizer metricsCommonTags() { + return registry -> registry.config().commonTags("application", "my-app"); + } +} diff --git a/src/main/resources/application-actuator.yml b/src/main/resources/application-actuator.yml new file mode 100644 index 00000000..167d9532 --- /dev/null +++ b/src/main/resources/application-actuator.yml @@ -0,0 +1,12 @@ +management: + endpoints: + web: + exposure: + include: prometheus,health,metrics + endpoint: + health: + show-details: always + prometheus: + metrics: + export: + enabled: true \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7d298092..05b96e17 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,7 +5,7 @@ spring: resources: add-mappings: true profiles: - active: dev + active: dev,actuator security: oauth2: client: @@ -27,7 +27,7 @@ spring: allowed-origins: http://localhost:63342,http://localhost:8080,http://localhost:5500,http://localhost:5173,https://investfuture.my endpoints: public: - paths: /api/login,/api/asset,/api/oauth2,/api/healthcheck,/api/coin/realtime,/api/coin/min,/api/minute-ohlc,/v3/api-docs,/swagger,/swagger-ui,/swagger-ui.html,/swagger-resources,/webjars,/h2-console,/favicon.ico + paths: /api/login,/api/asset,/api/oauth2,/api/healthcheck,/api/coin/realtime,/api/coin/min,/api/minute-ohlc,/v3/api-docs,/swagger,/swagger-ui,/swagger-ui.html,/swagger-resources,/webjars,/h2-console,/favicon.ico,/actuator,/test websocket: paths: /api/coin/min,/api/coin/realtime,/api/coin/orderbook jwt: From cf35215d011fdceb191563da84a4be70fcdac3f8 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Sun, 8 Jun 2025 00:58:05 +0900 Subject: [PATCH 03/11] =?UTF-8?q?test:=20refresher=20=EC=BD=94=EB=93=9C=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 --- .../realitybot/api/UnitPriceRefresher.java | 3 +- .../realitybot/api/RefresherRunnerTest.java | 33 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java index db3f2c8f..6c5aff3e 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java @@ -10,6 +10,7 @@ import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import java.util.List; @@ -17,7 +18,7 @@ import java.util.concurrent.ConcurrentHashMap; @Slf4j -@Service +@Component @RequiredArgsConstructor public class UnitPriceRefresher implements ApplicationRunner { private final UnitPricePolicy unitPricePolicy; diff --git a/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java b/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java index cfb7bf79..a54b5647 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java @@ -1,24 +1,47 @@ package com.cleanengine.coin.realitybot.api; +import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; +import com.cleanengine.coin.realitybot.parser.OpeningPriceParser; +import com.cleanengine.coin.realitybot.vo.UnitPricePolicy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; +import java.util.Collections; + import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; -@SpringBootTest +@ExtendWith(MockitoExtension.class) public class RefresherRunnerTest { - @SpyBean + + @Spy + @InjectMocks private UnitPriceRefresher unitPriceRefresher; + @Mock + private AssetRepository assetRepository; + @Mock + private ApplicationArguments applicationArguments; + + @BeforeEach + public void setUp(){ + when(assetRepository.findAll()).thenReturn(Collections.emptyList()); + } @DisplayName("어플리케이션 실행 시 호가 단위 수집") @Test - public void runwithrefrecher(){ + public void runwithrefrecher() { + unitPriceRefresher.run(applicationArguments); + verify(unitPriceRefresher,times(1)).run(any(ApplicationArguments.class)); verify(unitPriceRefresher,times(1)).initializeUnitPrices(); } From 4856ed7143f5ca5a8048a30c48a1a59d366d096f Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Sun, 8 Jun 2025 00:59:57 +0900 Subject: [PATCH 04/11] =?UTF-8?q?test:=20refresher=20=EC=BD=94=EB=93=9C=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 --- .../cleanengine/coin/realitybot/api/UnitPriceRefresher.java | 1 - .../cleanengine/coin/realitybot/api/RefresherRunnerTest.java | 5 ----- 2 files changed, 6 deletions(-) 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 6c5aff3e..eafe3a3a 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/UnitPriceRefresher.java @@ -11,7 +11,6 @@ import org.springframework.boot.ApplicationRunner; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; 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 a54b5647..ef27e0bc 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/api/RefresherRunnerTest.java @@ -1,8 +1,6 @@ package com.cleanengine.coin.realitybot.api; import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; -import com.cleanengine.coin.realitybot.parser.OpeningPriceParser; -import com.cleanengine.coin.realitybot.vo.UnitPricePolicy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,9 +10,6 @@ import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; import java.util.Collections; From 918b2659b0f2701031d6fe8e9b73fe6103ff0022 Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Mon, 9 Jun 2025 03:51:43 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=EC=B2=B4=EA=B2=B0=EB=9F=89=20?= =?UTF-8?q?=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 | 9 +- .../domain/VWAPMetricsRecorder.java | 57 +++++ .../service/OrderGenerateService.java | 20 +- .../service/OrderGenerateServicebefore.java | 229 ++++++++++++++++++ .../service/PlatformVWAPService.java | 56 ++++- .../service/VWAPerrorInJectionScheduler.java | 48 ++++ .../realitybot/vo/DeviationPricePolicy.java | 18 +- .../coin/realitybot/vo/OrderVolumePolicy.java | 17 +- .../trade/repository/TradeRepository.java | 1 + src/main/resources/application-dev.yml | 2 +- src/main/resources/application.yml | 2 +- .../service/PlatformVWAPServiceTest.java | 4 +- 12 files changed, 435 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateServicebefore.java create mode 100644 src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.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 3aa66874..d8ad20c0 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -4,11 +4,14 @@ import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository; import com.cleanengine.coin.order.domain.Asset; import com.cleanengine.coin.realitybot.domain.APIVWAPState; +import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder; import com.cleanengine.coin.realitybot.dto.Ticks; import com.cleanengine.coin.realitybot.parser.TickParser; import com.cleanengine.coin.realitybot.service.OrderGenerateService; +import com.cleanengine.coin.realitybot.service.OrderGenerateServicebefore; import com.cleanengine.coin.realitybot.service.TickServiceManager; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -32,6 +35,7 @@ public class ApiScheduler { private final Map lastSequentialIdMap = new ConcurrentHashMap<>(); private final AssetRepository assetRepository; private final CoinoneAPIClient coinoneAPIClient; + private final VWAPMetricsRecorder recorder; private final MeterRegistry meterRegistry; private String ticker; @@ -69,7 +73,10 @@ public void MarketDataRequest(String ticker){ lastSequentialIdMap.put(ticker,lastSeqId); double vwap = apiVWAPState.getVWAP(); double volume = apiVWAPState.getAvgVolumePerOrder(); + recorder.recordApiVwap(ticker,vwap); + orderGenerateService.generateOrder(ticker,vwap,volume); //1tick 당 매수/매도 3개씩 제작 + // log.info("작동확인 {}의 가격 : {} , 볼륨 : {}",ticker, vwap, volume); } @@ -96,7 +103,7 @@ public void destroy() throws Exception { //담긴 Queue데이터 확인용 } catch (Exception e) { log.error("Bithumb API 오류 발생: {} → Coinone으로 대체 요청", e.getMessage()); - return coinoneAPIClient.get(ticker); + return coinoneAPIClient.geta(ticker); } }*/ diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java new file mode 100644 index 00000000..6cb0fb49 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java @@ -0,0 +1,57 @@ +package com.cleanengine.coin.realitybot.domain; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +@Component +@RequiredArgsConstructor +public class VWAPMetricsRecorder { + private final MeterRegistry meterRegistry; + private final ConcurrentHashMap> orderPriceMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> apiVwapMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> platformVwapMap = new ConcurrentHashMap<>(); + +// @Bean +// public MeterRegistryCustomizer metricsCommonTags() { +// return registry -> registry.config() +// .commonTags("application", "my-app"); // 모든 메트릭에 자동 추가 +// } + + + public void recordPrice(String ticker, boolean isBuy, double price){ + String type = isBuy ? "buy" : "sell"; + String timeStamp = Instant.now().toString(); + String key = ticker +"|"+type+"|"+timeStamp; + + AtomicReference value = new AtomicReference<>(price); + + meterRegistry.gauge("order_price", + Tags.of("ticker",ticker,"type",type,"timestamp",timeStamp) + ,value,AtomicReference::get); + orderPriceMap.put(key, value); + } + + public void recordApiVwap(String ticker, double price){ + apiVwapMap.computeIfAbsent(ticker, t -> { + AtomicReference ref = new AtomicReference<>(price); + meterRegistry.gauge("api_vwap",Tags.of("ticker",t),ref,AtomicReference::get); + return ref; + }).set(price); + } + public void recordPlatformVwap(String ticker, double price){ + platformVwapMap.computeIfAbsent(ticker, t -> { + AtomicReference ref = new AtomicReference<>(price); + meterRegistry.gauge("platform_vwap",Tags.of("ticker",t),ref,AtomicReference::get); + return ref; + }).set(price); + } + +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java index c8c0bbb2..04610a44 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -2,6 +2,7 @@ import com.cleanengine.coin.order.application.OrderService; import com.cleanengine.coin.realitybot.api.UnitPriceRefresher; +import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder; import com.cleanengine.coin.realitybot.vo.DeviationPricePolicy; import com.cleanengine.coin.realitybot.vo.OrderPricePolicy; import com.cleanengine.coin.realitybot.vo.OrderVolumePolicy; @@ -29,6 +30,7 @@ @Order(5) @RequiredArgsConstructor public class OrderGenerateService { + private final VWAPMetricsRecorder VWAPMetricsRecorder; @Value("${bot-handler.order-level}") private int[] orderLevels; //체결 강도 private double unitPrice = 0; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요 @@ -41,6 +43,9 @@ public class OrderGenerateService { private final OrderVolumePolicy orderVolumePolicy; private final OrderWalletRepository orderWalletRepository; private final OrderAccountRepository accountExternalRepository; + private final VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler; + + private final VWAPMetricsRecorder recorder; private String ticker; @@ -50,15 +55,15 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// //호가 정책 적용 this.unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker); - //최근 체결 내역 가져오기 - List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); +// //최근 체결 내역 가져오기 +// List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); // Platform 기반 가격 생성 (10개 이하, 10개 이상에 따른 가격 생성) - double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,trades,apiVWAP); - + double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,apiVWAP); + recorder.recordPlatformVwap(ticker,platformVWAP); //편차 계산 (vwap 기준) double trendLineRate = (platformVWAP - apiVWAP)/ apiVWAP; - + vwaPerrorInJectionScheduler.enableInjection(); for(int level : orderLevels) { //1주문당 3회 매수매도 처리 OrderPricePolicy.OrderPrice basePrice = orderPricePolicy.calculatePrice(level,platformVWAP,unitPrice,trendLineRate); DeviationPricePolicy.AdjustPrice adjustPrice = deviationPricePolicy.adjust( @@ -79,7 +84,7 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// } catch (InterruptedException e) { throw new RuntimeException(e); } - /* DecimalFormat df = new DecimalFormat("#,##0.00"); + DecimalFormat df = new DecimalFormat("#,##0.00"); DecimalFormat dfv = new DecimalFormat("#,###.########"); //모니터링용 System.out.println("sellPrice = " + df.format(sellPrice)); @@ -90,7 +95,6 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// System.out.println("===================================="); System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP)); -*/ } /*System.out.println("📦"+ticker+" [체결 기록 Top 10]"); trades.forEach(t -> @@ -106,7 +110,7 @@ private void createOrderWithFallback(String ticker,boolean isBuy, double volume, new DecimalFormat("#,###.########").format(volume)); return; } - + recorder.recordPrice(ticker,isBuy,price); try { orderService.createOrderWithBot(ticker, isBuy, volume, price); } catch (IllegalArgumentException e) { diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateServicebefore.java b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateServicebefore.java new file mode 100644 index 00000000..ba9b8580 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateServicebefore.java @@ -0,0 +1,229 @@ +package com.cleanengine.coin.realitybot.service; + +import com.cleanengine.coin.common.error.DomainValidationException; +import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository; +import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository; +import com.cleanengine.coin.order.application.OrderService; +import com.cleanengine.coin.realitybot.api.UnitPriceRefresher; +import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder; +import com.cleanengine.coin.trade.entity.Trade; +import com.cleanengine.coin.trade.repository.TradeRepository; +import com.cleanengine.coin.user.domain.Account; +import com.cleanengine.coin.user.domain.Wallet; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +import java.text.DecimalFormat; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; +import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; + +@Slf4j +@Service +@Order(5) +@RequiredArgsConstructor +public class OrderGenerateServicebefore { + @Value("${bot-handler.order-level}") + private int[] orderLevels; //체결 강도 + private double unitPrice = 0; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요 + private final UnitPriceRefresher unitPriceRefresher; + private final PlatformVWAPService platformVWAPService; + private final OrderService orderService; + private final TradeRepository tradeRepository; + private final OrderWalletRepository orderWalletRepository; + private final OrderAccountRepository accountExternalRepository; + private final VWAPMetricsRecorder recorder; + private final VWAPerrorInJectionScheduler inJectionScheduler; + private String ticker; + + + public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//기준 주문금액, 주문량 받기 (tick당 계산되어 들어옴) + this.ticker = ticker; + + //호가 정책 적용 + this.unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker); + +// //최근 체결 내역 가져오기 +// List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); + + // Platform 기반 가격 생성 (10개 이하, 10개 이상에 따른 가격 생성) + double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,apiVWAP); + recorder.recordPlatformVwap(ticker,platformVWAP); + + inJectionScheduler.enableInjection(); + //편차 계산 (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); + } + + //주문 실행 + 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(); //에러 발생기 비활성화 + + DecimalFormat df = new DecimalFormat("#,##0.00"); + //모니터링용 + System.out.println("sellPrice = " + df.format(sellPrice)); + System.out.println("sellVolume = " + sellVolume); + //모니터링용 + System.out.println("buyPrice = " + df.format(buyPrice)); + System.out.println("buyVolume = " + buyVolume); + + System.out.println("===================================="); + System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP)); + + } + + } + + private void createOrderWithFallback(String ticker,boolean isBuy, double volume, double price ) { + if (volume <= 0 || price <= 0){ + log.error("잘못된 주문이 발생 [종목 : {}] ,[isBuy : {}] ,[금액 : {}] ,[수량 : {}] 주문은 생성 취소",ticker,isBuy, + new DecimalFormat("#,###.########").format(price), + new DecimalFormat("#,###.########").format(volume)); + return; + } + + recorder.recordPrice(ticker, isBuy, price); + try { + orderService.createOrderWithBot(ticker, isBuy, volume, price); + } catch (DomainValidationException e) { + log.debug("잔량 부족: {}", e.getMessage()); + try { + resetBot(ticker); + orderService.createOrderWithBot(ticker, isBuy, volume, price); + } catch (Exception e1) { + log.error("주문 재시도 실패", e1); + } + } + } + + protected void resetBot(String ticker){ + this.ticker = ticker; + Wallet wallet = orderWalletRepository.findWalletBy(SELL_ORDER_BOT_ID,ticker).get(); + wallet.setSize(500_000_000.0); + Wallet wallet2 = orderWalletRepository.findWalletBy(BUY_ORDER_BOT_ID,ticker).get(); + wallet2.setSize(0.0); + orderWalletRepository.save(wallet); + orderWalletRepository.save(wallet2); + + Account account = accountExternalRepository.findByUserId(SELL_ORDER_BOT_ID).get(); + account.setCash(0.0); + Account account2 = accountExternalRepository.findByUserId(BUY_ORDER_BOT_ID).get(); + account2.setCash(500_000_000.0); + accountExternalRepository.save(account); + 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/PlatformVWAPService.java b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java index b6544433..a19a939d 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java @@ -2,25 +2,71 @@ import com.cleanengine.coin.realitybot.domain.PlatformVWAPState; import com.cleanengine.coin.trade.entity.Trade; +import com.cleanengine.coin.trade.repository.TradeRepository; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Service -@Slf4j +@RequiredArgsConstructor public class PlatformVWAPService {//TODO 가상 시장 조회용 사라질 예정임 + private final TradeRepository tradeRepository; + Map vwapMap = new ConcurrentHashMap<>(); + Map lastTradeTimeMap = new ConcurrentHashMap<>(); + - public double calculateVWAPbyTrades(String ticker,List trades,double apiVWAP) { + public double calculateVWAPbyTrades(String ticker,double apiVWAP) { PlatformVWAPState state = vwapMap.computeIfAbsent(ticker, PlatformVWAPState::new); - if (trades.size() < 10){ - //체결 내역이 10개 이하일 경우 자체 계산 - return generateVWAP(apiVWAP); + LocalDateTime lastTradeTime = lastTradeTimeMap.get(ticker); + + //최근 체결 내역 가져오기 + List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); + + if ( trades.size() < 10){ + //체결 내역이 10개 이하일 경우 자체 계산 + return generateVWAP(apiVWAP); + } + LocalDateTime newestTime = trades.get(0).getTradeTime(); + if (lastTradeTime == null) { + lastTradeTimeMap.put(ticker, newestTime); + state.addTrades(trades); + return state.getVWAP(); + } + boolean containsSameTime = false; + for (Trade trade : trades) { + if (trade.getTradeTime().isEqual(lastTradeTime)) { + containsSameTime = true; + break; } + } + System.out.println("✅ [갱신] 새로운 기준시간: " + newestTime); + System.out.println("🚨 [이전데이터] 기준시간 유지: " + lastTradeTime); + if (!containsSameTime) { + System.out.println("작동한다~~~"); + trades = tradeRepository.findByTickerAndTradeTimeGreaterThanEqualOrderByTradeTimeDesc(ticker, lastTradeTime); + newestTime = trades.get(0).getTradeTime(); + lastTradeTimeMap.put(ticker, newestTime); + System.out.println("🌹 [갱신] 10건 이상의 거래 기준으로: " + newestTime); + } + + //================= state.addTrades(trades); + + System.out.println("📦"+ticker+" [체결 기록]"); + state.addTrades(trades);trades.forEach(t -> + System.out.printf("🕒 %s | 가격: %.0f | 수량: %.8f | 매수: #%d ↔ 매도: #%d%n", + t.getTradeTime(), t.getPrice(), t.getSize(), t.getBuyUserId(), t.getSellUserId()) + ); + + return state.getVWAP(); } diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java b/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java new file mode 100644 index 00000000..059e24d1 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java @@ -0,0 +1,48 @@ +package com.cleanengine.coin.realitybot.service; + +import com.cleanengine.coin.trade.entity.Trade; +import com.cleanengine.coin.trade.repository.TradeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; +import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; + +@Component +@RequiredArgsConstructor +public class VWAPerrorInJectionScheduler { + + private final TradeRepository tradeRepository; + + private boolean shouldInject = false; + private boolean hasInjected = false; + + public void enableInjection() { + if (hasInjected) return; // 이미 실행한 적 있으면 무시 + this.shouldInject = true; + } + + @Scheduled(fixedRate = 30000) // 혹은 따로 수동 호출도 가능 + public void injectFakeTrade() { + if (!shouldInject || hasInjected) return; + + Trade fakeTrade = new Trade(); + fakeTrade.setTicker("TRUMP"); + fakeTrade.setBuyUserId(BUY_ORDER_BOT_ID); // 테스트용 유저 ID + fakeTrade.setSellUserId(SELL_ORDER_BOT_ID); // 테스트용 유저 ID + fakeTrade.setPrice(25000.0); // 말도 안되는 고가 (예: 시장 평균이 19,000일 때) +// fakeTrade.setPrice(18900.0); // 말도 안되는 고가 (예: 시장 평균이 19,000일 때) + fakeTrade.setSize(300.0); // 대량 체결 +// fakeTrade.setSize(100.0); // 대량 체결 + fakeTrade.setTradeTime(LocalDateTime.now()); + + tradeRepository.save(fakeTrade); + hasInjected = true; // ✅ 한 번 실행했으니 플래그 설정 + shouldInject = false; + + System.out.println("🚨 혼동 Trade 1건 삽입 완료!"); + } +} diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java index 523015a0..6ef0e471 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/DeviationPricePolicy.java @@ -17,13 +17,18 @@ public class DeviationPricePolicy { */ public AdjustPrice adjust(double platformSell,double platformBuy, double trendLineRate, double apiVWAP, double unitPrice){ - double deviation = Math.abs(trendLineRate); - - if (deviation <= 0.01){ + double deviation = Math.abs(trendLineRate);//음수값 보정 + if (deviation <= 0.017){ return new AdjustPrice(platformSell,platformBuy); } double weight = getCorrectionWeight(deviation); - double closeness = 0.5 + (weight * 0.3); // 보간 가중치: 0.7 ~ 1.0 -> 0.5 +// double closeness = 1-weight; // 보간 가중치: 0.7 ~ 1.0 -> 0.5 + double closeness; // 보간 가중치: 0.7 ~ 1.0 -> 0.5 + if (deviation > 0.07){ + closeness = Math.max(0.2, 1 - weight); + } else { + closeness = 0.01; + } // double targetVWAP = (trendLineRate > 0) //만약 closeness 를 0.5 입력시 중간값 // ? apiVWAP + (platformSell - apiVWAP) * closeness // 고평가 → platformSell(25000) → apiVWAP(16000) 사이 가중치 %로 유도 @@ -39,6 +44,8 @@ public AdjustPrice adjust(double platformSell,double platformBuy, double trendLi buyTarget = apiVWAP - (apiVWAP - platformBuy) * closeness; } +// double adjustedSell = normalizeToUnit(sellTarget,unitPrice); +// double adjustedBuy = normalizeToUnit(buyTarget,unitPrice); double adjustedSell = normalizeToUnit(interpolate(platformSell,sellTarget ,weight),unitPrice); double adjustedBuy = normalizeToUnit(interpolate(platformBuy,buyTarget ,weight),unitPrice); @@ -70,7 +77,8 @@ private double getCorrectionWeight(double deviation) { * 선형 보간 함수: platformPrice → apiVWAP 사이 보간 */ private double interpolate(double platformPrice, double apiVWAP, double weight) { - return platformPrice * (1 - weight) + apiVWAP * weight; + double interWeight = Math.max(1,weight*1.2); + return platformPrice * (1 - interWeight) + apiVWAP * interWeight; } private double normalizeToUnit(double price, double unitPrice) { return Math.round(price / unitPrice) * unitPrice; diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java index 3f43eacf..3a80839e 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java @@ -22,12 +22,19 @@ public double calculateVolume(double avgVolume, double trendLineRate, boolean is //편차에 따른 거래량 보정 3% -> 최대 2.5배 증가 double deviation = Math.abs(trendLineRate); //절댓값 반환 - if (deviation>0.01){ //1% 초과할 경우 - double power = deviation * 100; //0.03 -> 3% -// double multiplier = 1.0 + (power * 0.5); //2.5배 (max로 사용) - double multiplier = Math.pow(1.1,power); //2.5배 (max로 사용) - rawVolume *= multiplier; //강한 추세 -> 강한 보정 + double power = deviation * 100; //0.03 -> 3% + double multiplier; + if (deviation >= 0.1){//1% 초과할 경우 + multiplier = 1.0 + (power * 0.5); //2.5배 (max로 사용) + } else if (deviation >= 0.01){ + double baseline = 5.0-((deviation - 0.01)/0.09)*2.0; + multiplier = baseline + (power * 0.5); + } else { + multiplier = 1.0 + (power * 0.5); } +// double multiplier = Math.pow(1.2,power); //2.5배 (max로 사용) + rawVolume *= multiplier; //강한 추세 -> 강한 보정 + //매수-매도 비중 조정 if (deviation <=0.001) //0.1%일 경우 안정권 , 추가적인 보정 x diff --git a/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java b/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java index a8854cc6..30bc3de2 100644 --- a/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java +++ b/src/main/java/com/cleanengine/coin/trade/repository/TradeRepository.java @@ -23,4 +23,5 @@ public interface TradeRepository extends JpaRepository { List findByBuyUserIdAndTicker(Integer buyUserId, String ticker); List findBySellUserIdAndTicker(Integer sellUserId, String ticker); List findTop10ByTickerOrderByTradeTimeDesc(String ticker); + List findByTickerAndTradeTimeGreaterThanEqualOrderByTradeTimeDesc(String ticker, LocalDateTime lastTime); } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 6ec6fe43..7bb40686 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -28,6 +28,6 @@ spring: frontend: url: "http://localhost:5173/callback" bot-handler: - fixed-rate: 5000 # 5초마다 실행 + fixed-rate: 1000 # 5초마다 실행 corn : "0 0 0 * * *" # 매일 자정마다 호가 order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 05b96e17..3debdbbf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -45,6 +45,6 @@ server: forward-headers-strategy: native bot-handler: - fixed-rate: 5000 # 5초마다 실행 + fixed-rate: 1000 # 5초마다 실행 corn : "0 0 0 * * *" # 매일 자정마다 호가 order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 diff --git a/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java b/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java index b4138452..58105cc7 100644 --- a/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java +++ b/src/test/java/com/cleanengine/coin/realitybot/service/PlatformVWAPServiceTest.java @@ -38,7 +38,7 @@ void testCalculateVWAPLessThan10Trades() { double apiVWAP = 1000.0; //0.1%의 보정값 //when - double result = platformVWAPService.calculateVWAPbyTrades(ticker, trades, apiVWAP); + double result = platformVWAPService.calculateVWAPbyTrades(ticker, apiVWAP); //than assertEquals(apiVWAP, result,1); @@ -67,7 +67,7 @@ void testCalculateVWAPMoreThan10Trades() { when(platformVwapState.getVWAP()).thenReturn(15000.0); platformVWAPService.vwapMap.put(ticker, platformVwapState); //when - double result = platformVWAPService.calculateVWAPbyTrades(ticker, trades, apiVWAP); + double result = platformVWAPService.calculateVWAPbyTrades(ticker, apiVWAP); //then verify(platformVwapState).addTrades(trades); From 90ac9a7db6f9eef8cf27a27f49843db96377b307 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Tue, 10 Jun 2025 18:03:40 +0900 Subject: [PATCH 06/11] =?UTF-8?q?config:=20actuator=20yml=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/domain/VWAPMetricsRecorder.java | 3 +-- src/main/resources/application-actuator.yml | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java index 6cb0fb49..1bf4c081 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java @@ -3,8 +3,6 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; import lombok.RequiredArgsConstructor; -import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; -import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.time.Instant; @@ -39,6 +37,7 @@ public void recordPrice(String ticker, boolean isBuy, double price){ orderPriceMap.put(key, value); } + public void recordApiVwap(String ticker, double price){ apiVwapMap.computeIfAbsent(ticker, t -> { AtomicReference ref = new AtomicReference<>(price); diff --git a/src/main/resources/application-actuator.yml b/src/main/resources/application-actuator.yml index 167d9532..71f36a71 100644 --- a/src/main/resources/application-actuator.yml +++ b/src/main/resources/application-actuator.yml @@ -1,3 +1,7 @@ +spring: + config: + activate: + on-profile: metric management: endpoints: web: From 9b9672e309e0e9e3705fb8f6f57a9ac291af9bf3 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Tue, 10 Jun 2025 18:05:50 +0900 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=EB=B3=BC=EB=A5=A80=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=EC=A3=BC=EB=AC=B8=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java index 3a80839e..0c19925f 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java +++ b/src/main/java/com/cleanengine/coin/realitybot/vo/OrderVolumePolicy.java @@ -55,6 +55,9 @@ private double volumeExpansion(double rawVolume){ if(resultVolume <= 0) { //Volume이 0이하일 경우 재 계산 resultVolume = Math.round(rawVolume * 10000000.0) / 10000000.0; + if(resultVolume <= 0){ + resultVolume = 0.0000001; + } } return resultVolume; } From 81199dd365aabfd48dbc67f99fe01b394983c72f Mon Sep 17 00:00:00 2001 From: 109an94 <109an94@gmail.com> Date: Wed, 11 Jun 2025 02:59:22 +0900 Subject: [PATCH 08/11] =?UTF-8?q?refactor:=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EB=93=B1=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/ApiScheduler.java | 2 - .../coin/realitybot/config/MetricConfig.java | 3 + .../domain/VWAPMetricsRecorder.java | 7 - .../service/OrderGenerateService.java | 6 +- .../service/OrderGenerateServicebefore.java | 229 ------------------ .../service/PlatformVWAPService.java | 9 +- .../service/VWAPerrorInJectionScheduler.java | 48 ---- src/main/resources/application-actuator.yml | 4 - src/main/resources/application-dev.yml | 5 +- src/main/resources/application.yml | 4 +- 10 files changed, 11 insertions(+), 306 deletions(-) delete mode 100644 src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateServicebefore.java delete mode 100644 src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.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 d8ad20c0..a4c0a5cd 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -8,10 +8,8 @@ import com.cleanengine.coin.realitybot.dto.Ticks; import com.cleanengine.coin.realitybot.parser.TickParser; import com.cleanengine.coin.realitybot.service.OrderGenerateService; -import com.cleanengine.coin.realitybot.service.OrderGenerateServicebefore; import com.cleanengine.coin.realitybot.service.TickServiceManager; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java b/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java index a55b49c6..7eaf4177 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java +++ b/src/main/java/com/cleanengine/coin/realitybot/config/MetricConfig.java @@ -4,7 +4,9 @@ import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +//@Profile("actuator") @Configuration public class MetricConfig { @@ -12,4 +14,5 @@ public class MetricConfig { MeterRegistryCustomizer metricsCommonTags() { return registry -> registry.config().commonTags("application", "my-app"); } + } diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java index 1bf4c081..cb6cb16e 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java @@ -17,13 +17,6 @@ public class VWAPMetricsRecorder { private final ConcurrentHashMap> apiVwapMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap> platformVwapMap = new ConcurrentHashMap<>(); -// @Bean -// public MeterRegistryCustomizer metricsCommonTags() { -// return registry -> registry.config() -// .commonTags("application", "my-app"); // 모든 메트릭에 자동 추가 -// } - - public void recordPrice(String ticker, boolean isBuy, double price){ String type = isBuy ? "buy" : "sell"; String timeStamp = Instant.now().toString(); 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 04610a44..9e84c3df 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -43,7 +43,6 @@ public class OrderGenerateService { private final OrderVolumePolicy orderVolumePolicy; private final OrderWalletRepository orderWalletRepository; private final OrderAccountRepository accountExternalRepository; - private final VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler; private final VWAPMetricsRecorder recorder; private String ticker; @@ -63,7 +62,6 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// recorder.recordPlatformVwap(ticker,platformVWAP); //편차 계산 (vwap 기준) double trendLineRate = (platformVWAP - apiVWAP)/ apiVWAP; - vwaPerrorInJectionScheduler.enableInjection(); for(int level : orderLevels) { //1주문당 3회 매수매도 처리 OrderPricePolicy.OrderPrice basePrice = orderPricePolicy.calculatePrice(level,platformVWAP,unitPrice,trendLineRate); DeviationPricePolicy.AdjustPrice adjustPrice = deviationPricePolicy.adjust( @@ -84,7 +82,7 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// } catch (InterruptedException e) { throw new RuntimeException(e); } - DecimalFormat df = new DecimalFormat("#,##0.00"); +/* DecimalFormat df = new DecimalFormat("#,##0.00"); DecimalFormat dfv = new DecimalFormat("#,###.########"); //모니터링용 System.out.println("sellPrice = " + df.format(sellPrice)); @@ -94,7 +92,7 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// System.out.println("buyVolume = " + dfv.format(buyVolume)); System.out.println("===================================="); - System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP)); + System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP));*/ } /*System.out.println("📦"+ticker+" [체결 기록 Top 10]"); trades.forEach(t -> diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateServicebefore.java b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateServicebefore.java deleted file mode 100644 index ba9b8580..00000000 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateServicebefore.java +++ /dev/null @@ -1,229 +0,0 @@ -package com.cleanengine.coin.realitybot.service; - -import com.cleanengine.coin.common.error.DomainValidationException; -import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository; -import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository; -import com.cleanengine.coin.order.application.OrderService; -import com.cleanengine.coin.realitybot.api.UnitPriceRefresher; -import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder; -import com.cleanengine.coin.trade.entity.Trade; -import com.cleanengine.coin.trade.repository.TradeRepository; -import com.cleanengine.coin.user.domain.Account; -import com.cleanengine.coin.user.domain.Wallet; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Service; - -import java.text.DecimalFormat; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; -import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; - -@Slf4j -@Service -@Order(5) -@RequiredArgsConstructor -public class OrderGenerateServicebefore { - @Value("${bot-handler.order-level}") - private int[] orderLevels; //체결 강도 - private double unitPrice = 0; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요 - private final UnitPriceRefresher unitPriceRefresher; - private final PlatformVWAPService platformVWAPService; - private final OrderService orderService; - private final TradeRepository tradeRepository; - private final OrderWalletRepository orderWalletRepository; - private final OrderAccountRepository accountExternalRepository; - private final VWAPMetricsRecorder recorder; - private final VWAPerrorInJectionScheduler inJectionScheduler; - private String ticker; - - - public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//기준 주문금액, 주문량 받기 (tick당 계산되어 들어옴) - this.ticker = ticker; - - //호가 정책 적용 - this.unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker); - -// //최근 체결 내역 가져오기 -// List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker); - - // Platform 기반 가격 생성 (10개 이하, 10개 이상에 따른 가격 생성) - double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,apiVWAP); - recorder.recordPlatformVwap(ticker,platformVWAP); - - inJectionScheduler.enableInjection(); - //편차 계산 (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); - } - - //주문 실행 - 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(); //에러 발생기 비활성화 - - DecimalFormat df = new DecimalFormat("#,##0.00"); - //모니터링용 - System.out.println("sellPrice = " + df.format(sellPrice)); - System.out.println("sellVolume = " + sellVolume); - //모니터링용 - System.out.println("buyPrice = " + df.format(buyPrice)); - System.out.println("buyVolume = " + buyVolume); - - System.out.println("===================================="); - System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP)); - - } - - } - - private void createOrderWithFallback(String ticker,boolean isBuy, double volume, double price ) { - if (volume <= 0 || price <= 0){ - log.error("잘못된 주문이 발생 [종목 : {}] ,[isBuy : {}] ,[금액 : {}] ,[수량 : {}] 주문은 생성 취소",ticker,isBuy, - new DecimalFormat("#,###.########").format(price), - new DecimalFormat("#,###.########").format(volume)); - return; - } - - recorder.recordPrice(ticker, isBuy, price); - try { - orderService.createOrderWithBot(ticker, isBuy, volume, price); - } catch (DomainValidationException e) { - log.debug("잔량 부족: {}", e.getMessage()); - try { - resetBot(ticker); - orderService.createOrderWithBot(ticker, isBuy, volume, price); - } catch (Exception e1) { - log.error("주문 재시도 실패", e1); - } - } - } - - protected void resetBot(String ticker){ - this.ticker = ticker; - Wallet wallet = orderWalletRepository.findWalletBy(SELL_ORDER_BOT_ID,ticker).get(); - wallet.setSize(500_000_000.0); - Wallet wallet2 = orderWalletRepository.findWalletBy(BUY_ORDER_BOT_ID,ticker).get(); - wallet2.setSize(0.0); - orderWalletRepository.save(wallet); - orderWalletRepository.save(wallet2); - - Account account = accountExternalRepository.findByUserId(SELL_ORDER_BOT_ID).get(); - account.setCash(0.0); - Account account2 = accountExternalRepository.findByUserId(BUY_ORDER_BOT_ID).get(); - account2.setCash(500_000_000.0); - accountExternalRepository.save(account); - 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/PlatformVWAPService.java b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java index a19a939d..1a56ab60 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java @@ -47,24 +47,21 @@ public double calculateVWAPbyTrades(String ticker,double apiVWAP) { break; } } - System.out.println("✅ [갱신] 새로운 기준시간: " + newestTime); - System.out.println("🚨 [이전데이터] 기준시간 유지: " + lastTradeTime); + if (!containsSameTime) { - System.out.println("작동한다~~~"); trades = tradeRepository.findByTickerAndTradeTimeGreaterThanEqualOrderByTradeTimeDesc(ticker, lastTradeTime); newestTime = trades.get(0).getTradeTime(); lastTradeTimeMap.put(ticker, newestTime); - System.out.println("🌹 [갱신] 10건 이상의 거래 기준으로: " + newestTime); } //================= state.addTrades(trades); - System.out.println("📦"+ticker+" [체결 기록]"); + /*System.out.println("📦"+ticker+" [체결 기록]"); state.addTrades(trades);trades.forEach(t -> System.out.printf("🕒 %s | 가격: %.0f | 수량: %.8f | 매수: #%d ↔ 매도: #%d%n", t.getTradeTime(), t.getPrice(), t.getSize(), t.getBuyUserId(), t.getSellUserId()) - ); + );*/ return state.getVWAP(); diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java b/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java deleted file mode 100644 index 059e24d1..00000000 --- a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.cleanengine.coin.realitybot.service; - -import com.cleanengine.coin.trade.entity.Trade; -import com.cleanengine.coin.trade.repository.TradeRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; - -import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; -import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; - -@Component -@RequiredArgsConstructor -public class VWAPerrorInJectionScheduler { - - private final TradeRepository tradeRepository; - - private boolean shouldInject = false; - private boolean hasInjected = false; - - public void enableInjection() { - if (hasInjected) return; // 이미 실행한 적 있으면 무시 - this.shouldInject = true; - } - - @Scheduled(fixedRate = 30000) // 혹은 따로 수동 호출도 가능 - public void injectFakeTrade() { - if (!shouldInject || hasInjected) return; - - Trade fakeTrade = new Trade(); - fakeTrade.setTicker("TRUMP"); - fakeTrade.setBuyUserId(BUY_ORDER_BOT_ID); // 테스트용 유저 ID - fakeTrade.setSellUserId(SELL_ORDER_BOT_ID); // 테스트용 유저 ID - fakeTrade.setPrice(25000.0); // 말도 안되는 고가 (예: 시장 평균이 19,000일 때) -// fakeTrade.setPrice(18900.0); // 말도 안되는 고가 (예: 시장 평균이 19,000일 때) - fakeTrade.setSize(300.0); // 대량 체결 -// fakeTrade.setSize(100.0); // 대량 체결 - fakeTrade.setTradeTime(LocalDateTime.now()); - - tradeRepository.save(fakeTrade); - hasInjected = true; // ✅ 한 번 실행했으니 플래그 설정 - shouldInject = false; - - System.out.println("🚨 혼동 Trade 1건 삽입 완료!"); - } -} diff --git a/src/main/resources/application-actuator.yml b/src/main/resources/application-actuator.yml index 71f36a71..167d9532 100644 --- a/src/main/resources/application-actuator.yml +++ b/src/main/resources/application-actuator.yml @@ -1,7 +1,3 @@ -spring: - config: - activate: - on-profile: metric management: endpoints: web: diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 7bb40686..d204d6a6 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -27,7 +27,4 @@ spring: frontend: url: "http://localhost:5173/callback" -bot-handler: - fixed-rate: 1000 # 5초마다 실행 - corn : "0 0 0 * * *" # 매일 자정마다 호가 - order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3debdbbf..990a53c0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,7 +5,7 @@ spring: resources: add-mappings: true profiles: - active: dev,actuator + active: dev security: oauth2: client: @@ -45,6 +45,6 @@ server: forward-headers-strategy: native bot-handler: - fixed-rate: 1000 # 5초마다 실행 + fixed-rate: 5000 # 5초마다 실행 corn : "0 0 0 * * *" # 매일 자정마다 호가 order-level : 1,2,3,4,5 #오더북 단계 설정 - 주문량 증가 From f0980de943ecbc0cff2b5dc9219ed393b9e35ec9 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Wed, 11 Jun 2025 13:00:56 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20metric=20overhead=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/VWAPMetricsRecorder.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java index cb6cb16e..1d960704 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/VWAPMetricsRecorder.java @@ -1,5 +1,6 @@ package com.cleanengine.coin.realitybot.domain; +import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; import lombok.RequiredArgsConstructor; @@ -14,20 +15,35 @@ public class VWAPMetricsRecorder { private final MeterRegistry meterRegistry; private final ConcurrentHashMap> orderPriceMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap orderPriceSummery = new ConcurrentHashMap<>(); private final ConcurrentHashMap> apiVwapMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap> platformVwapMap = new ConcurrentHashMap<>(); public void recordPrice(String ticker, boolean isBuy, double price){ String type = isBuy ? "buy" : "sell"; String timeStamp = Instant.now().toString(); - String key = ticker +"|"+type+"|"+timeStamp; +// String key = ticker +"|"+type+"|"+timeStamp; + String key = ticker +"|"+type; - AtomicReference value = new AtomicReference<>(price); - meterRegistry.gauge("order_price", - Tags.of("ticker",ticker,"type",type,"timestamp",timeStamp) - ,value,AtomicReference::get); - orderPriceMap.put(key, value); + AtomicReference priceRef = orderPriceMap.computeIfAbsent(key, k -> { + + AtomicReference value = new AtomicReference<>(price); + + meterRegistry.gauge("order_price", + Tags.of("ticker",ticker,"type",type) + ,value,AtomicReference::get); + return value; + }); + priceRef.set(price); + + DistributionSummary summary = orderPriceSummery.computeIfAbsent(key,k -> + DistributionSummary.builder("order_price_summary") + .tags(Tags.of("ticker",ticker,"type",type)) + .publishPercentiles(0.05,0.95) + .register(meterRegistry) + ); + summary.record(price); } From 57ee983c7a7773786f15f54ab006c77f9b9297e1 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Wed, 11 Jun 2025 14:31:40 +0900 Subject: [PATCH 10/11] =?UTF-8?q?refactor:=20thread-sleep=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coin/realitybot/api/ApiScheduler.java | 1 - .../realitybot/service/OrderGenerateService.java | 15 ++------------- .../realitybot/service/PlatformVWAPService.java | 3 --- 3 files changed, 2 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 a4c0a5cd..345806a7 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java +++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java @@ -45,7 +45,6 @@ public void MarketAllRequest() throws InterruptedException { for (Asset ticker : tickers) { String tickerName = ticker.getTicker(); MarketDataRequest(tickerName); -// Thread.sleep(500); } }); } 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 9e84c3df..17c0599d 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java @@ -1,15 +1,13 @@ package com.cleanengine.coin.realitybot.service; +import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository; +import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository; import com.cleanengine.coin.order.application.OrderService; import com.cleanengine.coin.realitybot.api.UnitPriceRefresher; import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder; import com.cleanengine.coin.realitybot.vo.DeviationPricePolicy; import com.cleanengine.coin.realitybot.vo.OrderPricePolicy; import com.cleanengine.coin.realitybot.vo.OrderVolumePolicy; -import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository; -import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository; -import com.cleanengine.coin.trade.entity.Trade; -import com.cleanengine.coin.trade.repository.TradeRepository; import com.cleanengine.coin.user.domain.Account; import com.cleanengine.coin.user.domain.Wallet; import lombok.RequiredArgsConstructor; @@ -19,8 +17,6 @@ import org.springframework.stereotype.Service; import java.text.DecimalFormat; -import java.util.List; -import java.util.concurrent.TimeUnit; import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID; import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID; @@ -37,7 +33,6 @@ public class OrderGenerateService { private final UnitPriceRefresher unitPriceRefresher; private final PlatformVWAPService platformVWAPService; private final OrderService orderService; - private final TradeRepository tradeRepository; private final OrderPricePolicy orderPricePolicy; private final DeviationPricePolicy deviationPricePolicy; private final OrderVolumePolicy orderVolumePolicy; @@ -76,12 +71,6 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {// createOrderWithFallback(ticker,false, sellVolume, sellPrice); createOrderWithFallback(ticker,true, buyVolume, buyPrice); - - try { - TimeUnit.MICROSECONDS.sleep(100); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } /* DecimalFormat df = new DecimalFormat("#,##0.00"); DecimalFormat dfv = new DecimalFormat("#,###.########"); //모니터링용 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 1a56ab60..6aadb643 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java +++ b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java @@ -4,12 +4,9 @@ import com.cleanengine.coin.trade.entity.Trade; import com.cleanengine.coin.trade.repository.TradeRepository; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; From 2ea0862cfaef0160421ccd0c2a32f32d5cac2aa8 Mon Sep 17 00:00:00 2001 From: 109an <109an94@gmail.com> Date: Wed, 11 Jun 2025 14:34:35 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20NPE=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cleanengine/coin/realitybot/domain/APIVWAPState.java | 4 +++- src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java b/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java index fa22e48b..aa801573 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java +++ b/src/main/java/com/cleanengine/coin/realitybot/domain/APIVWAPState.java @@ -17,8 +17,10 @@ public void addTick(Ticks tick){ if (ticksQueue.size() >= maxQueueSize) { //10개 이상이 되면 선착순으로 제거해나감 Ticks removed = ticksQueue.poll(); + if (removed != null){ calculator.removeTrade(removed.getTrade_price(), removed.getTrade_volume()); - } + } + } //초기엔 들어온 갯수에 따라 증가시켜서 계산함 ticksQueue.add(tick); calculator.recordTrade(tick.getTrade_price(),tick.getTrade_volume()); diff --git a/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java b/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java index 9513aab6..438246e6 100644 --- a/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java +++ b/src/main/java/com/cleanengine/coin/realitybot/dto/Ticks.java @@ -12,7 +12,7 @@ public class Ticks { private String trade_date_utc; // LocalDate private String trade_time_utc; // LocalTime private String timestamp; //instant 에러 발생 - private float trade_price; + private double trade_price; private double trade_volume; private float prev_closing_price; private double change_price;