diff --git a/.github/workflows/code-analyze-sonarqube.yml b/.github/workflows/code-analyze-sonarqube.yml
new file mode 100644
index 00000000..5c258ca8
--- /dev/null
+++ b/.github/workflows/code-analyze-sonarqube.yml
@@ -0,0 +1,61 @@
+name: Code Analyze With SonarQube
+
+run-name: Run code analyze triggered by ${{github.actor}}
+
+on:
+ pull_request:
+ types: [opened, reopened, synchronize]
+ branches:
+ - main
+ - dev
+ paths:
+ - 'src/**'
+ push:
+ branches:
+ - dev
+ paths:
+ - 'src/**'
+
+jobs:
+ build:
+ name: Build and analyze
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: 21
+ distribution: 'temurin'
+
+ - name: Cache SonarQube packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.sonar/cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+
+ - name: Cache Gradle packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
+ restore-keys: ${{ runner.os }}-gradle
+
+ - name: Build with tests
+ env:
+ JWT_SECRET: ${{ secrets.JWT_SECRET }}
+ continue-on-error: true
+ run: ./gradlew build --info
+
+ - name: SonarQube Analysis
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
+ JWT_SECRET: ${{ secrets.JWT_SECRET }}
+ run: ./gradlew sonar --info
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 0dd67a8a..e6c01564 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,10 +1,13 @@
-name: CI/CD Deploy
+name: CI/CD Deploy and Start services
-run-name: Deploy springboot as backend image to ECR by ${{github.actor}}
+run-name: Deploy to ECR and Start services by ${{github.actor}}
on:
push:
- branches: [ "main" ]
+ branches:
+ - main
+ paths:
+ - 'src/**'
jobs:
build:
@@ -29,20 +32,31 @@ jobs:
- name: Build with Gradle Wrapper
run: ./gradlew build -x test
- - name: Build image
- run: |
- docker build -t ${{ secrets.AWS_ORGANIZATION }}/backend .
- docker tag ${{ secrets.AWS_ORGANIZATION }}/backend:latest ${{ secrets.AWS_ECR_BACKEND_REPO }}:latest
-
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v4
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
with:
- role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
- aws-region: ${{ secrets.AWS_REGION }}
+ username: ${{secrets.DOCKER_USERNAME}}
+ password: ${{secrets.DOCKER_TOKEN}}
- - name: Login to Amazon ECR
- uses: aws-actions/amazon-ecr-login@v2
+ - name: Build and Push Docker Image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ tags: ${{secrets.DOCKER_USERNAME}}/if-be:latest
- - name: Push image to ECR
- run: |
- docker push ${{ secrets.AWS_ECR_BACKEND_REPO }}:latest
\ No newline at end of file
+ start-services:
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Connect to cloud server and run Docker commands
+ uses: appleboy/ssh-action@v1.2.2
+ with:
+ host: ${{secrets.BACKEND_HOST}}
+ username: ${{secrets.CLOUD_USERNAME}}
+ key: ${{secrets.CLOUD_SECRET_KEY}}
+ port: ${{secrets.CLOUD_PORT}}
+ script: |
+ cd ~
+ /bin/bash run_springboot.sh
diff --git a/.github/workflows/start-service.yml b/.github/workflows/start-service.yml
deleted file mode 100644
index 90f6cdff..00000000
--- a/.github/workflows/start-service.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: Start services
-
-run-name: Start containers in cloud service by ${{ github.actor }}
-
-on:
- workflow_run:
- workflows: [CI/CD Deploy]
- types:
- - completed
-
-jobs:
- start-services:
- runs-on: ubuntu-latest
- steps:
- - name: Connect to cloud server
- uses: appleboy/ssh-action@v1.2.2
- with:
- host: ${{secrets.CLOUD_HOST}}
- username: ${{secrets.CLOUD_USERNAME}}
- key: ${{secrets.CLOUD_SECRET_KEY}}
- port: ${{secrets.CLOUD_PORT}}
- script: |
- cd ~
- sudo docker compose stop backend
- sudo docker compose rm -f backend
- sudo docker compose pull backend
- sudo docker compose up -d backend
diff --git a/README.md b/README.md
deleted file mode 100644
index 522858a7..00000000
--- a/README.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# Invest Future
-> 만약에 이때(IF) 투자했다면 지금 얼마를 벌었을까?
->
-> 항상 저희는 상상을 하곤 합니다. ~~(이 때 돈 넣었으면 지금 4배는 벌었는데)~~
-> 하지만 현실은 돈이 부족하기 떄문에 투자를 하기 쉽지 않습니다.
->
-> IF에서는 모의투자를 통해 투자에 대한 경험을 쌓고, 투자에 대한 이해를 높일 수 있습니다.
-
-- 배포 URL : https://investfuture.my
-
-
-## 다른 모의투자 서비스와의 차이점
- 많은 모의투자서비스는 3가지 유형이 있습니다.
- 1. 턴제 모의투자
- 2. 실시간 모의투자 (거래가 차트에 반영되지 않음)
- 3. 자체 시장 모의투자 (거래가 차트에 반영 됨)
-
-
-
-
-
-
-
-
-
-
-
-
-
시장 개입 여부
-
DB 지원 여부
-
봇 지원 여부
-
-
-
-
-
-기존 모의투자 서비스는 실제 거래소의 시세를 지원하면 사용자의 투자 행동이 차트에 반영되지 않거나,
-차트에 반영되지만 실제 시장과는 다른 자체 시장을 형성하는 서비스가 제공되고 있습니다.
-
- Invest Future(IF)는 실제 시장의 추세를 따라가고, 사용자의 투자 행동이 차트에 반영되는 모의투자 서비스입니다.
-
-
-
-
-
-
-## How it works?
-
-> 기존 실시간 모의투자 서비스에서 사용자의 투자 행동이 차트에 반영되지 않는 이유는 사용자의 투자 행동이 차트에 반영될경우 시간이 지날수록 실제 시장의 차트와 모의투자 서비스의 차트의 괴리가 커지기 때문입니다.
-> 비유하자면 시간이 지날수록 멀티버스가 발생합니다.
-
-
-Invest Future(IF)는 사용자의 투자 행동으로 서비스의 차트와 실제 시장의 차트의 괴리가 커지면 자체 매수봇과 매도봇이 매수 매도를 하면서 실제 시장과의 차이를 보간합니다.
-
-
-
-
-## 프로젝트 특징
-
-
-
-### 세부 로직
-
- 주문/체결
-
-
-
설명 :
-
-
-
-
- 보간
-
-
-
설명 :
-
-
-
-
- 차트/DB
-
-
-
설명 :
-
-
-
-
-
-## 1차 구현 [5.2 ~ 5.15]
-
-코인 한 개(트럼프 코인)을 매수 할 수 있습니다.
-사용자의 주문에 따라 그래프차트와 호가창, 실시간 체결창에 반영됩니다.
-
-https://github.com/user-attachments/assets/779b77f1-e778-4a53-b37f-d79a2dc187e8
diff --git a/build.gradle b/build.gradle
index bf5d0264..f3e70cbb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,8 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.7'
+ id "org.sonarqube" version "6.0.1.5171"
+ id 'jacoco'
}
group = 'com.cleanengine'
@@ -23,6 +25,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
@@ -45,8 +67,13 @@ dependencies {
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
+ testImplementation 'org.springframework.boot:spring-boot-testcontainers:3.4.5'
+ testImplementation 'org.testcontainers:junit-jupiter'
+ testImplementation 'org.testcontainers:mariadb'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+ testImplementation 'org.junit.platform:junit-platform-suite:1.10.0'
// Spring Security + OAuth2
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
@@ -58,5 +85,22 @@ dependencies {
}
tasks.named('test') {
- useJUnitPlatform()
+ useJUnitPlatform{
+ excludeTags 'testcontainers'
+ }
+ finalizedBy jacocoTestReport
}
+
+sonar {
+ properties {
+ property "sonar.projectKey", "CleanEngine_cleanengine-be_2b6f2f63-fa39-426c-b9c7-8aa127fd14d8"
+ property "sonar.projectName", "cleanengine-be"
+ property "sonar.host.url", System.getenv('SONAR_HOST_URL') ?: 'http://localhost:9000'
+ property "sonar.token", System.getenv('SONAR_TOKEN') ?: ''
+ property "sonar.java.source", '21'
+ property "sonar.java.target", '21'
+ property "sonar.sourceEncoding", "UTF-8"
+ property "sonar.java.coveragePlugin", "jacoco"
+ property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/test/jacocoTestReport.xml"
+ }
+}
\ No newline at end of file
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 00000000..2c7e82ef
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,51 @@
+version: '3.8'
+
+services:
+ app:
+ image: eclipse-temurin:21-jre-alpine
+ container_name: my-spring-app
+ volumes:
+ - ../build/libs/coin-0.0.1-SNAPSHOT.jar:/app/coin-0.0.1-SNAPSHOT.jar
+ - /etc/localtime:/etc/localtime:ro
+ - /home/ubuntu/logs/springboot:/app/logs
+ working_dir: /app
+ command: ["java", "-jar", "coin-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev,mariadb-local"]
+ ports:
+ - "8080:8080"
+ env_file:
+ - ./local.properties
+ environment:
+ - TZ=Asia/Seoul
+ depends_on:
+ mariadb:
+ condition: service_healthy
+ networks:
+ - app-network
+
+ mariadb:
+ image: mariadb:latest
+ container_name: mariadb
+ ports:
+ - "3306:3306"
+ env_file:
+ - ./local.properties
+ environment:
+ - TZ=Asia/Seoul
+ volumes:
+ - mariadb_data:/var/lib/mysql
+ - ./mariadb/init.sql:/docker-entrypoint-initdb.d/init.sql
+ healthcheck:
+ test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ networks:
+ - app-network
+
+networks:
+ app-network:
+ driver: bridge
+
+volumes:
+ mariadb_data:
+ driver: local
diff --git a/docker/mariadb/init.sql b/docker/mariadb/init.sql
new file mode 100644
index 00000000..824352af
--- /dev/null
+++ b/docker/mariadb/init.sql
@@ -0,0 +1,106 @@
+create table account
+(
+ account_id int auto_increment
+ primary key,
+ cash double not null,
+ user_id int not null
+);
+
+create table asset
+(
+ ticker varchar(10) not null
+ primary key,
+ name varchar(100) null,
+ icon blob null
+);
+
+create table buy_orders
+(
+ buy_order_id bigint auto_increment
+ primary key,
+ created_at datetime(6) not null,
+ is_bot bit not null,
+ is_marketorder bit not null,
+ order_size double null,
+ price double null,
+ remaining_size double null,
+ state enum ('CANCELED', 'DONE', 'WAIT') not null,
+ ticker varchar(10) not null,
+ user_id int not null,
+ locked_deposit double not null,
+ remaining_deposit double not null
+);
+
+create table oauth
+(
+ oauth_id int auto_increment
+ primary key,
+ access_token text null,
+ email varchar(255) null,
+ expires_at datetime(6) null,
+ nickname varchar(255) null,
+ provider varchar(20) not null,
+ provider_user_id varchar(255) not null,
+ refresh_token text null,
+ scope varchar(255) null,
+ user_id int not null
+);
+
+create table sell_orders
+(
+ sell_order_id bigint auto_increment
+ primary key,
+ created_at datetime(6) not null,
+ is_bot bit not null,
+ is_marketorder bit not null,
+ order_size double null,
+ price double null,
+ remaining_size double null,
+ state enum ('CANCELED', 'DONE', 'WAIT') not null,
+ ticker varchar(10) not null,
+ user_id int not null
+);
+
+create table trade
+(
+ trade_id int auto_increment
+ primary key,
+ buy_user_id int not null,
+ price double not null,
+ sell_user_id int not null,
+ size double not null,
+ ticker varchar(255) not null,
+ trade_time datetime(6) not null
+);
+
+create table users
+(
+ user_id int auto_increment
+ primary key,
+ created_at datetime(6) not null
+);
+
+create table wallet
+(
+ wallet_id bigint auto_increment
+ primary key,
+ account_id int not null,
+ buy_price double null,
+ roi double null,
+ size double not null,
+ ticker varchar(10) not null
+);
+
+
+
+INSERT INTO `if`.users (user_id, created_at) VALUES (1, '2025-05-16 09:30:00.000000');
+INSERT INTO `if`.users (user_id, created_at) VALUES (2, '2025-05-16 09:30:00.000000');
+
+INSERT INTO `if`.account (account_id, cash, user_id) VALUES (1, 0, 1);
+INSERT INTO `if`.account (account_id, cash, user_id) VALUES (2, 500000000, 2);
+
+INSERT INTO `if`.asset (ticker, name) VALUES ('BTC', '비트코인');
+INSERT INTO `if`.asset (ticker, name) VALUES ('TRUMP', '오피셜트럼프');
+
+INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (1, 1, 0, 0, 500000000, 'BTC');
+INSERT INTO `if`.wallet (wallet_id, account_id, buy_price, roi, size, ticker) VALUES (2, 1, 0, 0, 500000000, 'TRUMP');
diff --git a/src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java b/src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java
index d237d1f5..b13f113c 100644
--- a/src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java
+++ b/src/main/java/com/cleanengine/coin/chart/controller/ChartDataController.java
@@ -4,7 +4,7 @@
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
import com.cleanengine.coin.chart.service.ChartSubscriptionService;
import com.cleanengine.coin.chart.service.RealTimeOhlcService;
-import com.cleanengine.coin.common.annotation.WorkingServerProfile;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
@@ -12,9 +12,11 @@
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
@Component
-@WorkingServerProfile
+//@WorkingServerProfile
@RequiredArgsConstructor
@Slf4j
public class ChartDataController {
@@ -22,6 +24,10 @@ public class ChartDataController {
private final ChartSubscriptionService subscriptionService;
private final SimpMessagingTemplate messagingTemplate;
private final RealTimeOhlcService realTimeOhlcService;
+ // 티커별 마지막으로 전송한 OHLC 데이터 캐싱
+ @Getter
+ private final Map lastSentOhlcDataMap = new ConcurrentHashMap<>();
+
/**
* 1초마다 실행 - 실시간 OHLC 데이터 전송
@@ -46,20 +52,36 @@ public void publishRealTimeOhlc() {
RealTimeOhlcDto ohlcData = realTimeOhlcService.getRealTimeOhlc(ticker);
if (ohlcData == null) {
- log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 빈 데이터 전송", ticker);
- RealTimeOhlcDto emptyData = new RealTimeOhlcDto();
- emptyData.setTicker(ticker);
- emptyData.setTimestamp(LocalDateTime.now());
- emptyData.setOpen(0.0);
- emptyData.setHigh(0.0);
- emptyData.setLow(0.0);
- emptyData.setClose(0.0);
- emptyData.setVolume(0.0);
-
- messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
+ // 이전에 전송한 데이터가 있는지 확인
+ RealTimeOhlcDto lastSentData = lastSentOhlcDataMap.get(ticker);
+
+ if (lastSentData != null) {
+ // 이전 데이터가 있으면 타임스탬프만 업데이트하여 재사용
+ log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 이전 데이터 재사용", ticker);
+ RealTimeOhlcDto updatedData = new RealTimeOhlcDto(lastSentData.getTicker(), LocalDateTime.now(), // 현재 시간으로 업데이트
+ lastSentData.getOpen(), lastSentData.getHigh(), lastSentData.getLow(), lastSentData.getClose(), lastSentData.getVolume());
+
+ messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, updatedData);
+ lastSentOhlcDataMap.put(ticker, updatedData); // 캐시 업데이트
+ } else {
+ // 이전 데이터도 없는 경우 빈 데이터 전송 (첫 구독 시)
+ log.debug("티커 {}의 이전 OHLC 데이터도 없습니다. 빈 데이터 전송", ticker);
+ RealTimeOhlcDto emptyData = new RealTimeOhlcDto();
+ emptyData.setTicker(ticker);
+ emptyData.setTimestamp(LocalDateTime.now());
+ emptyData.setOpen(0.0);
+ emptyData.setHigh(0.0);
+ emptyData.setLow(0.0);
+ emptyData.setClose(0.0);
+ emptyData.setVolume(0.0);
+
+ messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
+ lastSentOhlcDataMap.put(ticker, emptyData); // 캐시 업데이트
+ }
} else {
// 조회된 실시간 OHLC 데이터 전송
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, ohlcData);
+ lastSentOhlcDataMap.put(ticker, ohlcData); // 캐시 업데이트
log.debug("실시간 OHLC 데이터 전송: {}", ohlcData);
}
} catch (Exception e) {
@@ -69,6 +91,7 @@ public void publishRealTimeOhlc() {
} catch (Exception e) {
log.error("△ 실시간 OHLC 데이터 발행 중 오류: {}", e.getMessage(), e);
}
+
}
diff --git a/src/main/java/com/cleanengine/coin/chart/controller/PrevRateController.java b/src/main/java/com/cleanengine/coin/chart/controller/PrevRateController.java
index 87220608..414a1dd4 100644
--- a/src/main/java/com/cleanengine/coin/chart/controller/PrevRateController.java
+++ b/src/main/java/com/cleanengine/coin/chart/controller/PrevRateController.java
@@ -1,12 +1,10 @@
package com.cleanengine.coin.chart.controller;
-import com.cleanengine.coin.chart.dto.PrevRateDto;
-import com.cleanengine.coin.chart.service.RealTimeDataPrevRateService;
+import com.cleanengine.coin.chart.service.ChartSubscriptionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
-import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
@@ -14,21 +12,10 @@
@Slf4j
public class PrevRateController {
- private final RealTimeDataPrevRateService prevRateService;
- private final SimpMessagingTemplate messagingTemplate;
+ private final ChartSubscriptionService chartSubscriptionService;
@MessageMapping("/subscribe/prevRate/{ticker}")
public void subscribePrevRate(@DestinationVariable String ticker) {
- log.debug("티커 {} 전일 대비 변동률 구독 요청", ticker);
-
- // 서비스를 통해 전일 대비 변동률 데이터 얻기
- PrevRateDto data = prevRateService.generatePrevRateData(ticker);
-
- // 티커별 토픽으로 전송
- messagingTemplate.convertAndSend(
- "/topic/prevRate/" + ticker,
- data
- );
- log.debug("전송 완료: /topic/prevRate/{} -> {}", ticker, data);
+ chartSubscriptionService.subscribePrevRate(ticker);
}
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/controller/PrevRateScheduler.java b/src/main/java/com/cleanengine/coin/chart/controller/PrevRateScheduler.java
deleted file mode 100644
index b45e1f2a..00000000
--- a/src/main/java/com/cleanengine/coin/chart/controller/PrevRateScheduler.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.cleanengine.coin.chart.controller;
-
-import com.cleanengine.coin.common.annotation.WorkingServerProfile;
-import lombok.RequiredArgsConstructor;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-@Component
-@WorkingServerProfile
-@RequiredArgsConstructor
-public class PrevRateScheduler {
- private final PrevRateController prevRateController;
-
- @Scheduled(fixedDelay = 1000)
- public void sendPrevRate(){
- prevRateController.subscribePrevRate("TRUMP");
- }
-}
diff --git a/src/main/java/com/cleanengine/coin/chart/controller/RealTimeRateScheduler.java b/src/main/java/com/cleanengine/coin/chart/controller/RealTimeRateScheduler.java
deleted file mode 100644
index a4c8bb4b..00000000
--- a/src/main/java/com/cleanengine/coin/chart/controller/RealTimeRateScheduler.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.cleanengine.coin.chart.controller;
-
-import com.cleanengine.coin.common.annotation.WorkingServerProfile;
-import lombok.RequiredArgsConstructor;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-@Component
-@WorkingServerProfile
-@RequiredArgsConstructor
-public class RealTimeRateScheduler {
- private final RealTimeTradeController realTimeTradeController;
-
- @Scheduled(fixedDelay = 5000)
- public void sendPrevRate(){
- realTimeTradeController.realTimeTradeRate("TRUMP");
- }
-}
diff --git a/src/main/java/com/cleanengine/coin/chart/controller/RealTimeTradeController.java b/src/main/java/com/cleanengine/coin/chart/controller/RealTimeTradeController.java
index 4f3ed025..b3c1ec84 100644
--- a/src/main/java/com/cleanengine/coin/chart/controller/RealTimeTradeController.java
+++ b/src/main/java/com/cleanengine/coin/chart/controller/RealTimeTradeController.java
@@ -1,12 +1,11 @@
package com.cleanengine.coin.chart.controller;
-import com.cleanengine.coin.chart.dto.RealTimeDataDto;
-import com.cleanengine.coin.chart.service.RealTimeTradeService;
+
+import com.cleanengine.coin.chart.service.ChartSubscriptionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
-import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
@@ -14,8 +13,7 @@
@Slf4j
public class RealTimeTradeController {
- private final SimpMessagingTemplate messagingTemplate;
- private final RealTimeTradeService realTimeTradeService;
+ private final ChartSubscriptionService chartSubscriptionService;
/**
* 클라이언트가 /app/subscribe/realTimeTradeRate/{ticker} 로 send() 하면
@@ -23,16 +21,6 @@ public class RealTimeTradeController {
*/
@MessageMapping("/subscribe/realTimeTradeRate/{ticker}")
public void realTimeTradeRate(@DestinationVariable String ticker) {
- log.debug("티커 {} 실시간 구독 요청", ticker);
-
- // 서비스로부터 DTO 생성
- RealTimeDataDto data = realTimeTradeService.generateRealTimeData(ticker);
-
- // 티커별 토픽으로 전송
- messagingTemplate.convertAndSend(
- "/topic/realTimeTradeRate/" + ticker,
- data
- );
- log.debug("전송 완료: /topic/realTimeTradeRate/{} -> {}", ticker, data);
+ chartSubscriptionService.subscribeRealTimeTradeRate(ticker);
}
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java b/src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java
index 07ac65d5..be07435d 100644
--- a/src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java
+++ b/src/main/java/com/cleanengine/coin/chart/controller/WebSocketMessageController.java
@@ -3,7 +3,9 @@
import com.cleanengine.coin.chart.dto.RealTimeOhlcDto;
import com.cleanengine.coin.chart.service.ChartSubscriptionService;
import com.cleanengine.coin.chart.service.RealTimeOhlcService;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
+import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
@@ -17,9 +19,9 @@
public class WebSocketMessageController {
private final ChartSubscriptionService subscriptionService;
-
private final RealTimeOhlcService realTimeOhlcService;
private final SimpMessagingTemplate messagingTemplate;
+ private final ChartDataController chartDataController;
/**
* 실시간 OHLC 데이터 구독 처리
@@ -35,13 +37,27 @@ public void subscribeRealTimeOhlc(RealTimeTradeMappingDto request) {
// 구독 즉시 최근 실시간 OHLC 데이터 전송
RealTimeOhlcDto latestOhlcData = realTimeOhlcService.getRealTimeOhlc(ticker);
+ RealTimeOhlcDto lastSentData = chartDataController.getLastSentOhlcDataMap().get(ticker);
+
if (latestOhlcData == null) {
- log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다.", ticker);
- // 데이터가 없으면 빈 데이터 전송
- messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, createEmptyRealTimeOhlcDto(ticker));
+ if (lastSentData != null) {
+ // 이전에 전송한 데이터가 있으면 재사용
+ log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 이전 데이터 재사용", ticker);
+ messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, lastSentData);
+ } else {
+ // 이전 데이터도 없는 경우 빈 데이터 전송
+ log.debug("티커 {}의 실시간 OHLC 데이터가 없습니다. 빈 데이터 전송", ticker);
+ RealTimeOhlcDto emptyData = createEmptyRealTimeOhlcDto(ticker);
+ messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, emptyData);
+ // 빈 데이터도 캐시에 저장
+ chartDataController.getLastSentOhlcDataMap().put(ticker, emptyData);
+ }
} else {
log.debug("티커 {}의 실시간 OHLC 데이터 전송: {}", ticker, latestOhlcData);
messagingTemplate.convertAndSend("/topic/realTimeOhlc/" + ticker, latestOhlcData);
+ // 데이터 캐시에 저장
+ chartDataController.getLastSentOhlcDataMap().put(ticker, latestOhlcData);
+
}
}
@@ -60,15 +76,10 @@ private RealTimeOhlcDto createEmptyRealTimeOhlcDto(String ticker) {
/**
* WebSocket 매핑용 DTO
*/
+ @Setter
+ @Getter
public static class RealTimeTradeMappingDto {
private String ticker;
- public String getTicker() {
- return ticker;
- }
-
- public void setTicker(String ticker) {
- this.ticker = ticker;
- }
}
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java b/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java
new file mode 100644
index 00000000..7b5d9270
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/chart/handler/TradeEventHandler.java
@@ -0,0 +1,84 @@
+package com.cleanengine.coin.chart.handler;
+
+import com.cleanengine.coin.chart.dto.PrevRateDto;
+import com.cleanengine.coin.chart.dto.RealTimeDataDto;
+import com.cleanengine.coin.chart.dto.TradeEventDto;
+import com.cleanengine.coin.chart.service.ChartSubscriptionService; // 의존성 추가
+import com.cleanengine.coin.chart.service.RealTimeDataPrevRateService;
+import com.cleanengine.coin.chart.service.RealTimeTradeService;
+import com.cleanengine.coin.chart.service.WebsocketSendService;
+import com.cleanengine.coin.trade.application.TradeExecutedEvent;
+import com.cleanengine.coin.trade.entity.Trade;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class TradeEventHandler {
+ private final RealTimeTradeService realTimeTradeService;
+ private final WebsocketSendService websocketSendService;
+ private final ChartSubscriptionService chartSubscriptionService; // 주입
+ private final RealTimeDataPrevRateService realTimeDataPrevRateService;
+ //event로 이벤틀 처리해야한다.
+ //eventListener는 void로 처리를 해야한다
+ @EventListener
+ public void handleTradeEvent(TradeExecutedEvent event) {
+ Trade trade = event.getTrade();
+ if (trade == null) {
+ log.warn("Trade 객체가 null");
+ return;
+ }
+
+ String ticker = trade.getTicker();
+ TradeEventDto tradeEventDto = getTradeEventDto(trade);
+ log.debug("TradeEventHandler 수신 : {}", trade);
+
+ // 실시간 데이터 체결내역
+ // 해당 종목에 대한 구독자가 있는지 확인
+ if (chartSubscriptionService.isSubscribedToRealTimeTradeRate(ticker)) {
+ log.debug("종목 {} 실시간 체결 정보 구독자 확인됨. 데이터 처리 및 전송 시작.", ticker);
+
+
+ //실시간 체결가 및 변동률 전송
+ try {
+ RealTimeDataDto dto = realTimeTradeService.generateRealTimeData(tradeEventDto);
+ websocketSendService.sendChangeRate(dto, dto.getTicker()); // dto.getTicker()는 이미 ticker와 동일
+ log.debug("실시간 체결가 및 변동률 업데이트 전송 완료 : {}", ticker);
+ } catch (Exception e) {
+ log.error("종목 {} 실시간 체결가 및 변동률 업데이트 전송 중 오류: {}", ticker, e.getMessage(), e);
+ }
+ } else {
+ log.debug("종목 {} 실시간 체결 정보 구독자 없음. 데이터 전송 생략.", ticker);
+ }
+
+ //전날 종가 변동률 전송
+ if(chartSubscriptionService.isSubscribedToPrevRate(ticker)) {
+ log.debug("종목 {} 전일 대비 변동률 구독자 확인됨. 데이터 전송 시작.", ticker);
+ try {
+ PrevRateDto dto = realTimeDataPrevRateService.generatePrevRateData(tradeEventDto);
+ websocketSendService.sendPrevRate(dto, dto.getTicker()); // dto.getTicker()는 이미 ticker와 동일
+ log.debug("종목 {} 전일 대비 변동률 전송 완료 : {}", ticker, dto);
+ } catch (Exception e) {
+ log.error("종목 {} 전일 대비 변동률 전송 중 오류: {}", ticker, e.getMessage(), e);
+ }
+ }else {
+ log.debug("종목 {} 전일 대비 변동률 구독자 없음. 데이터 전송 생략.", ticker);
+ }
+
+ }
+
+ @NotNull
+ public static TradeEventDto getTradeEventDto(Trade trade) {
+ return new TradeEventDto(
+ trade.getTicker(),
+ trade.getSize(),
+ trade.getPrice(),
+ trade.getTradeTime()
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java b/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java
index ef089115..26d3f177 100644
--- a/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java
+++ b/src/main/java/com/cleanengine/coin/chart/service/ChartSubscriptionService.java
@@ -9,61 +9,65 @@
@Service
@Slf4j
public class ChartSubscriptionService {
- // 구독된 티커 목록 관리
- private final Set subscribedTickers = ConcurrentHashMap.newKeySet();
// 실시간 OHLC 구독 목록 관리
+ //이건 종목에 따라 확장성을 고려해서 만든것 종목으로 불러오기
private final Set realTimeOhlcSubscribedTickers = ConcurrentHashMap.newKeySet();
- // 실시간 거래 구독 목록 관리
- private final Set realTimeTradeSubscribedTickers = ConcurrentHashMap.newKeySet();
+ // 실시간 체결 정보 구독 목록 관리
+ private final Set realTimeTradeRateSubscribedTickers = ConcurrentHashMap.newKeySet();
- /**
- * 티커 구독 추가 (캔들)
+
+ //전날 종가 데이터 구독 목록 관리
+ private final Set PrevRateSubscribedTickers = ConcurrentHashMap.newKeySet();
+
+
+ /*
+ 실시간 체결 내역 구독
*/
- public void subscribe(String ticker) {
- log.debug("캔들 티커 구독 추가: {}", ticker);
- subscribedTickers.add(ticker);
+ public void subscribeRealTimeTradeRate(String ticker) {
+ validateTicker(ticker);
+ log.debug("실시간 체결 정보 티커 구독 추가: {}", ticker);
+ realTimeTradeRateSubscribedTickers.add(ticker);
}
- /**
- * 티커 구독 해제 (캔들)
- */
- public void unsubscribe(String ticker) {
- log.debug("캔들 티커 구독 해제: {}", ticker);
- subscribedTickers.remove(ticker);
+ //구독 해지
+ public void unsubscribeRealTimeTradeRate(String ticker) {
+ validateTicker(ticker);
+ log.debug("실시간 체결 정보 티커 구독 해지: {}", ticker);
+ realTimeTradeRateSubscribedTickers.remove(ticker);
}
- /**
- * 모든 구독된 티커 조회 (캔들)
- */
- public Set getAllSubscribedTickers() {
- return subscribedTickers;
+ //모든 구독 종목 반환
+ public Set getAllRealTimeTradeRateSubscribedTickers() {
+ return realTimeTradeRateSubscribedTickers;
}
- /**
- * 해당 티커가 구독되었는지 확인 (캔들)
- */
- public boolean isSubscribed(String ticker) {
- return subscribedTickers.contains(ticker);
+ //종목에 대한 구독 여부
+ public boolean isSubscribedToRealTimeTradeRate(String ticker) {
+ if (ticker == null || ticker.trim().isEmpty()) {
+ return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리
+ }
+ return realTimeTradeRateSubscribedTickers.contains(ticker);
}
+
/**
* 실시간 OHLC 티커 구독 추가
*/
public void subscribeRealTimeOhlc(String ticker) {
+ validateTicker(ticker);
log.debug("실시간 OHLC 티커 구독 추가: {}", ticker);
realTimeOhlcSubscribedTickers.add(ticker);
}
- /**
- * 실시간 OHLC 티커 구독 해제
- */
public void unsubscribeRealTimeOhlc(String ticker) {
- log.debug("실시간 OHLC 티커 구독 해제: {}", ticker);
+ validateTicker(ticker);
+ log.debug("실시간 OHLC 티커 구독 해지: {}", ticker);
realTimeOhlcSubscribedTickers.remove(ticker);
}
+
/**
* 모든 실시간 OHLC 구독된 티커 조회
*/
@@ -71,40 +75,46 @@ public Set getAllRealTimeOhlcSubscribedTickers() {
return realTimeOhlcSubscribedTickers;
}
- /**
- * 해당 티커가 실시간 OHLC 구독되었는지 확인
- */
- public boolean isRealTimeOhlcSubscribed(String ticker) {
+ public boolean isSubscribedToRealTimeOhlc(String ticker) {
+ if (ticker == null || ticker.trim().isEmpty()) {
+ return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리
+ }
return realTimeOhlcSubscribedTickers.contains(ticker);
}
- /**
- * 실시간 거래 데이터 티커 구독 추가
+
+ /*
+ 전날 종가 변동률 구독 추가,삭제,조회
*/
- public void subscribeRealTime(String ticker) {
- log.debug("실시간 거래 데이터 티커 구독 추가: {}", ticker);
- realTimeTradeSubscribedTickers.add(ticker);
+ public void subscribePrevRate(String ticker) {
+ validateTicker(ticker);
+ log.debug("전날 종가 변동률 티커 구독 추가: {}", ticker);
+ PrevRateSubscribedTickers.add(ticker);
}
- /**
- * 실시간 거래 데이터 티커 구독 해제
- */
- public void unsubscribeRealTime(String ticker) {
- log.debug("실시간 거래 데이터 티커 구독 해제: {}", ticker);
- realTimeTradeSubscribedTickers.remove(ticker);
+ public void unsubscribePrevRate(String ticker) {
+ validateTicker(ticker);
+ log.debug("전날 종가 변동률 티커 구독 해지: {}", ticker);
+ PrevRateSubscribedTickers.remove(ticker);
}
- /**
- * 모든 실시간 거래 데이터 구독된 티커 조회
- */
- public Set getAllRealTimeSubscribedTickers() {
- return realTimeTradeSubscribedTickers;
+ public Set getAllPrevRateSubscribedTickers(String ticker) {
+ return PrevRateSubscribedTickers;
}
- /**
- * 해당 티커가 실시간 거래 데이터 구독되었는지 확인
- */
- public boolean isRealTimeSubscribed(String ticker) {
- return realTimeTradeSubscribedTickers.contains(ticker);
+ public boolean isSubscribedToPrevRate(String ticker) {
+ if (ticker == null || ticker.trim().isEmpty()) {
+ return false; // 유효하지 않은 티커는 구독되지 않은 것으로 처리
+ }
+ return PrevRateSubscribedTickers.contains(ticker);
}
+
+ //CCmap은 null을 허용시키기때문에 null 종목이 들어가도 npe발생안되는 이슈 테스트에서 발견
+ //검증 로직 추가
+ private void validateTicker(String ticker) {
+ if (ticker == null || ticker.trim().isEmpty()) {
+ throw new IllegalArgumentException("유효하지 않은 티커입니다: " + ticker);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/service/RealTimeDataPrevRateService.java b/src/main/java/com/cleanengine/coin/chart/service/RealTimeDataPrevRateService.java
index 5ce9b1e6..b4e7f17f 100644
--- a/src/main/java/com/cleanengine/coin/chart/service/RealTimeDataPrevRateService.java
+++ b/src/main/java/com/cleanengine/coin/chart/service/RealTimeDataPrevRateService.java
@@ -1,11 +1,12 @@
package com.cleanengine.coin.chart.service;
import com.cleanengine.coin.chart.dto.PrevRateDto;
+import com.cleanengine.coin.chart.dto.TradeEventDto;
import com.cleanengine.coin.chart.repository.RealTimeTradeRepository;
import com.cleanengine.coin.trade.entity.Trade;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@@ -15,44 +16,48 @@
@Slf4j
public class RealTimeDataPrevRateService {
- private final SimpMessagingTemplate messagingTemplate;
private final RealTimeTradeRepository tradeRepository;
- // 기존 메서드 - 컨트롤러에서 호출된 후 내부적으로 전송
- public void publishPrevRate(String ticker) {
- PrevRateDto data = generatePrevRateData(ticker);
- messagingTemplate.convertAndSend("/topic/prevRate/" + ticker, data);
- log.debug("전송 완료: /topic/prevRate/{} -> {}", ticker, data);
+ public PrevRateDto generatePrevRateData(TradeEventDto currentTrade) {
+ return generatePrevRateData(currentTrade, LocalDateTime.now());
}
- // 새로운 메서드 - 컨트롤러에서 데이터만 가져오기 위해 사용
- public PrevRateDto generatePrevRateData(String ticker) {
- // 전일 종가 계산
- LocalDateTime today = LocalDateTime.now();
- LocalDateTime yesterdayStart = today.minusDays(1).withHour(0).withMinute(0).withSecond(0);
- LocalDateTime yesterdayEnd = today.minusDays(1).withHour(23).withMinute(59).withSecond(59);
+ PrevRateDto generatePrevRateData(TradeEventDto currentTrade, LocalDateTime currentTime) {
+ String ticker = currentTrade.getTicker();
+ YesterDay yesterDay = getYesterDay(currentTime);
+ log.debug("조회 시간 범위: {} ~ {}", yesterDay.yesterdayStart(), yesterDay.yesterdayEnd());
Trade yesterdayLastTrade = tradeRepository.findFirstByTickerAndTradeTimeBetweenOrderByTradeTimeDesc(
- ticker, yesterdayStart, yesterdayEnd);
-
- // 현재가
- Trade currentTrade = tradeRepository.findFirstByTickerOrderByTradeTimeDesc(ticker);
+ ticker, yesterDay.yesterdayStart(), yesterDay.yesterdayEnd());
- if (yesterdayLastTrade == null || currentTrade == null) {
- log.debug("전일 또는 현재 거래 데이터가 없습니다: {}", ticker);
- return new PrevRateDto(ticker, 0.0, 0.0, 0.0, LocalDateTime.now());
+ if (yesterdayLastTrade == null) {
+ log.debug("전일 거래 데이터가 없습니다: {}", ticker);
+ return new PrevRateDto(ticker, 0.0, currentTrade.getPrice(), 0.0, currentTime);
}
-
double prevClose = yesterdayLastTrade.getPrice();
double currentPrice = currentTrade.getPrice();
- double changeRate = ((currentPrice - prevClose) / prevClose) * 100;
+ double changeRate = getChangeRate(currentPrice, prevClose);
return new PrevRateDto(
ticker,
prevClose,
currentPrice,
changeRate,
- LocalDateTime.now()
+ currentTime
);
}
+
+ static double getChangeRate(double currentPrice, double prevClose) {
+ return ((currentPrice - prevClose) / prevClose) * 100;
+ }
+
+ @NotNull
+ static YesterDay getYesterDay(LocalDateTime today) {
+ LocalDateTime yesterdayStart = today.minusDays(1).withHour(0).withMinute(0).withSecond(0);
+ LocalDateTime yesterdayEnd = today.minusDays(1).withHour(23).withMinute(59).withSecond(59);
+ return new YesterDay(yesterdayStart, yesterdayEnd);
+ }
+
+ record YesterDay(LocalDateTime yesterdayStart, LocalDateTime yesterdayEnd) {
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java b/src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java
index af7bd463..19298eb1 100644
--- a/src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java
+++ b/src/main/java/com/cleanengine/coin/chart/service/RealTimeOhlcService.java
@@ -5,10 +5,10 @@
import com.cleanengine.coin.trade.repository.TradeRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -31,56 +31,90 @@ public class RealTimeOhlcService {
*/
public RealTimeOhlcDto getRealTimeOhlc(String ticker) {
try {
- // 현재 시간
LocalDateTime now = LocalDateTime.now();
- // 마지막 처리 시간 (없으면 현재 시간에서 1초 전)
- LocalDateTime lastProcessedTime = lastProcessedTimeMap.getOrDefault(
- ticker, now.minusSeconds(1));
+ // 시간 범위 계산
+ TimeRange timeRange = calculateTimeRange(ticker, now);
- // 1초 전부터 현재까지의 데이터 조회
- List recentTrades = tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(
- ticker,
- lastProcessedTime,
- now
- );
+ // 거래 데이터 조회 및 전처리
+ List recentTrades = getProcessedTradeData(ticker, timeRange);
- // 시간 순서대로 정렬이 필요하면 뒤집음
- Collections.reverse(recentTrades);
-
- // 거래 데이터가 없으면 마지막으로 캐싱된 데이터 반환
+ // 거래 데이터가 없으면 캐시된 데이터 반환
if (recentTrades.isEmpty()) {
- return lastOhlcDataMap.getOrDefault(ticker, null);
+ return getCachedData(ticker);
}
- // 새로운 마지막 처리 시간 업데이트
- lastProcessedTimeMap.put(ticker, now);
-
- // OHLC 계산
- Double open = recentTrades.get(0).getPrice();
- Double high = recentTrades.stream().mapToDouble(Trade::getPrice).max().orElse(0.0);
- Double low = recentTrades.stream().mapToDouble(Trade::getPrice).min().orElse(0.0);
- Double close = recentTrades.get(recentTrades.size() - 1).getPrice();
- Double volume = recentTrades.stream().mapToDouble(Trade::getSize).sum();
-
- // RealTimeOhlcDto 생성
- RealTimeOhlcDto ohlcData = new RealTimeOhlcDto(
- ticker,
- now,
- open,
- high,
- low,
- close,
- volume
- );
-
- // 캐시에 저장
- lastOhlcDataMap.put(ticker, ohlcData);
+ calculateOhlcv ohlcv = getCalculateOhlcv(recentTrades);
+
+ RealTimeOhlcDto ohlcData = createOhlcDto(ticker, now, ohlcv);
+
+ // 캐시 업데이트
+ updateCache(ticker, now, ohlcData);
return ohlcData;
} catch (Exception e) {
log.error("실시간 OHLC 데이터 생성 중 오류: {}", e.getMessage(), e);
- return lastOhlcDataMap.getOrDefault(ticker, null);
+ return getCachedData(ticker);
}
}
+
+ // 시간 범위 계산
+ TimeRange calculateTimeRange(String ticker, LocalDateTime now) {
+ LocalDateTime lastProcessedTime = lastProcessedTimeMap.getOrDefault(
+ ticker, now.minusSeconds(1));
+ return new TimeRange(lastProcessedTime, now);
+ }
+
+ // 거래 데이터 조회 및 전처리
+ List getProcessedTradeData(String ticker, TimeRange timeRange) {
+ return tradeRepository.findByTickerAndTradeTimeBetweenOrderByTradeTimeAsc(
+ ticker,
+ timeRange.start(),
+ timeRange.end()
+ );
+ }
+
+ // 캐시 업데이트
+ void updateCache(String ticker, LocalDateTime now, RealTimeOhlcDto ohlcData) {
+ lastProcessedTimeMap.put(ticker, now);
+ lastOhlcDataMap.put(ticker, ohlcData);
+ }
+
+ // 캐시된 데이터 조회
+ RealTimeOhlcDto getCachedData(String ticker) {
+ return lastOhlcDataMap.getOrDefault(ticker, null);
+ }
+
+ // DTO 생성
+ RealTimeOhlcDto createOhlcDto(String ticker, LocalDateTime timestamp, calculateOhlcv ohlcv) {
+ return new RealTimeOhlcDto(
+ ticker,
+ timestamp,
+ ohlcv.open(),
+ ohlcv.high(),
+ ohlcv.low(),
+ ohlcv.close(),
+ ohlcv.volume()
+ );
+ }
+
+ // OHLCV 계산 메서드
+ @NotNull
+ static calculateOhlcv getCalculateOhlcv(List trades) {
+ // trades는 시간 오름차순 정렬되어 있음
+ Double open = trades.getFirst().getPrice(); // 첫 번째(가장 오래된) = Open ✅
+ Double close = trades.getLast().getPrice(); // 마지막(가장 최근) = Close ✅
+ Double high = trades.stream().mapToDouble(Trade::getPrice).max().orElse(0.0);
+ Double low = trades.stream().mapToDouble(Trade::getPrice).min().orElse(0.0);
+ Double volume = trades.stream().mapToDouble(Trade::getSize).sum();
+
+ return new calculateOhlcv(open, high, low, close, volume);
+ }
+
+
+
+
+ record TimeRange(LocalDateTime start, LocalDateTime end) {}
+
+ record calculateOhlcv(Double open, Double high, Double low, Double close, Double volume) {}
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/service/RealTimeTradeService.java b/src/main/java/com/cleanengine/coin/chart/service/RealTimeTradeService.java
index 6b191261..54c6eb57 100644
--- a/src/main/java/com/cleanengine/coin/chart/service/RealTimeTradeService.java
+++ b/src/main/java/com/cleanengine/coin/chart/service/RealTimeTradeService.java
@@ -1,14 +1,13 @@
+
package com.cleanengine.coin.chart.service;
import com.cleanengine.coin.chart.dto.RealTimeDataDto;
import com.cleanengine.coin.chart.dto.TradeEventDto;
-import com.cleanengine.coin.trade.application.TradeBatchProcessor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.UUID;
-
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -17,81 +16,140 @@
@RequiredArgsConstructor
@Slf4j
public class RealTimeTradeService {
- //tradeService를 받아야한다
- private final TradeBatchProcessor tradeBatchProcessor;
-
- //실시간 거래 데이터 보안성에 적합한 ConcuerrentHashMap 사용
+ //실시간 거래 데이터 보안성에 적합한 ConcurrentHashMap 사용
//전에 데이터를 캐싱처리해서 변동률 계산
private final Map previousTradeMap = new ConcurrentHashMap<>();
- public RealTimeDataDto generateRealTimeData(String ticker){
- // 최신 거래 이벤트 데이터 조회
- TradeEventDto tradeEventDto = tradeBatchProcessor.retrieveTradeEventDto(ticker);
+ //이벤트 Dto 받아서 체결내역에 필요한 데이터들을 보내주는것
+ public RealTimeDataDto generateRealTimeData(TradeEventDto tradeEventDto) {
+ try {
+ TradeInfo currentTradeInfo = extractTradeInfo(tradeEventDto);
+
+ ChangeRateResult changeRateResult = calculateChangeRate(tradeEventDto, currentTradeInfo);
+
+ updateTradeCache(tradeEventDto, changeRateResult);
+
+ return createRealTimeDataDto(currentTradeInfo, changeRateResult.changeRate());
- // 거래 데이터가 없는 경우 처리
- if(tradeEventDto == null){
- log.debug("실시간 거래 데이터가 존재하지않습니다: {}", ticker);
- return new RealTimeDataDto(ticker, 0, 0, 0, LocalDateTime.now(), UUID.randomUUID().toString());
+ } catch (Exception e) {
+ log.error("실시간 데이터 생성 중 오류: {}", e.getMessage(), e);
+ // 기본값으로 DTO 생성
+ TradeInfo currentTradeInfo = extractTradeInfo(tradeEventDto);
+ return createRealTimeDataDto(currentTradeInfo, 0.0);
}
+ }
+
+ // 거래 정보 추출
+ TradeInfo extractTradeInfo(TradeEventDto tradeEventDto) {
+ return new TradeInfo(
+ tradeEventDto.getTicker(),
+ tradeEventDto.getPrice(),
+ tradeEventDto.getSize(),
+ tradeEventDto.getTimestamp()
+ );
+ }
+
+ // 변동률 계산
+ ChangeRateResult calculateChangeRate(TradeEventDto currentTrade, TradeInfo currentTradeInfo) {
+ TradeEventDto previousTrade = previousTradeMap.get(currentTradeInfo.ticker());
- // 현재 가격 및 시간 정보 추출
- double currentPrice = tradeEventDto.getPrice();
- double currentSize = tradeEventDto.getSize();
- LocalDateTime currentTime = tradeEventDto.getTimestamp();
+ logTradeComparison(currentTrade, previousTrade);
- // 변동률 계산
- double changeRate = 0.0;
- TradeEventDto previousTrade = previousTradeMap.get(ticker);
- //참조 오류로 동일한 객체를 보고있어서 변동률 계산에 오류가 발생
+ if (!shouldCalculateChangeRate(previousTrade, currentTrade)) {
+ return new ChangeRateResult(0.0, false);
+ }
+
+ if (!isNewTrade(previousTrade, currentTrade)) {
+ log.debug("동일한 타임스탬프의 거래 데이터가 다시 수신됨: {}", currentTrade.getTimestamp());
+ return new ChangeRateResult(0.0, false);
+ }
+
+ double changeRate = getChangeRate(currentTradeInfo.price(), previousTrade.getPrice());
+ log.debug("변동률 계산: 현재가={}, 이전가={}, 변동률={}%",
+ currentTradeInfo.price(), previousTrade.getPrice(), changeRate);
+
+ return new ChangeRateResult(changeRate, true);
+ }
+
+ // 변동률 계산 조건 검사
+ boolean shouldCalculateChangeRate(TradeEventDto previousTrade, TradeEventDto currentTrade) {
+ if (previousTrade == null) {
+ log.debug("이전 거래 정보가 없어 변동률을 0으로 설정: {}", currentTrade.getTicker());
+ return false;
+ }
+
+ if (previousTrade.getPrice() <= 0) {
+ log.debug("이전 거래 가격이 유효하지 않음: {}", previousTrade.getPrice());
+ return false;
+ }
+
+ if (previousTrade == currentTrade) {
+ log.debug("동일한 거래 객체가 다시 수신됨 (참조 동일): {}", currentTrade.getTicker());
+ return false;
+ }
+
+ return true;
+ }
+
+ // 새로운 거래인지 판단
+ boolean isNewTrade(TradeEventDto previousTrade, TradeEventDto currentTrade) {
+ if (previousTrade.getTimestamp() == null || currentTrade.getTimestamp() == null) {
+ return true;
+ }
+ return !previousTrade.getTimestamp().equals(currentTrade.getTimestamp());
+ }
+
+ // 거래 비교 로그
+ void logTradeComparison(TradeEventDto currentTrade, TradeEventDto previousTrade) {
log.debug("타임스탬프 비교 - 현재: {}, 이전: {}, 동일객체: {}",
- tradeEventDto.getTimestamp(),
+ currentTrade.getTimestamp(),
previousTrade != null ? previousTrade.getTimestamp() : "없음",
- previousTrade == tradeEventDto);
-
- // 이전 거래가 있고, 새로운 거래 데이터인 경우에만 변동률 계산
- if (previousTrade != null && previousTrade.getPrice() > 0 && previousTrade != tradeEventDto) {
- // 타임스탬프 비교로 새로운 거래인지 확인
- if (previousTrade.getTimestamp() == null || tradeEventDto.getTimestamp() == null ||
- !previousTrade.getTimestamp().equals(tradeEventDto.getTimestamp())) {
-
- double previousPrice = previousTrade.getPrice();
- changeRate = ((currentPrice - previousPrice) / previousPrice) * 100;
- log.debug("변동률 계산: 현재가={}, 이전가={}, 변동률={}%",
- currentPrice, previousPrice, changeRate);
-
- // 새로운 거래 데이터 저장
- previousTradeMap.put(ticker, new TradeEventDto(
- tradeEventDto.getTicker(),
- tradeEventDto.getSize(),
- tradeEventDto.getPrice(),
- tradeEventDto.getTimestamp()
- ));
- } else {
- log.debug("동일한 타임스탬프의 거래 데이터가 다시 수신됨: {}", tradeEventDto.getTimestamp());
- }
- } else {
- if (previousTrade == null) {
- log.debug("이전 거래 정보가 없어 변동률을 0으로 설정: {}", ticker);
- // 첫 거래 데이터 저장 (복사본 저장)
- previousTradeMap.put(ticker, new TradeEventDto(
- tradeEventDto.getTicker(),
- tradeEventDto.getSize(),
- tradeEventDto.getPrice(),
- tradeEventDto.getTimestamp()
- ));
- } else if (previousTrade == tradeEventDto) {
- log.debug("동일한 거래 객체가 다시 수신됨 (참조 동일): {}", ticker);
- }
+ previousTrade == currentTrade);
+ }
+
+ // 캐시 업데이트
+ void updateTradeCache(TradeEventDto tradeEventDto, ChangeRateResult changeRateResult) {
+ if (changeRateResult.shouldUpdate() || !previousTradeMap.containsKey(tradeEventDto.getTicker())) {
+ TradeEventDto cachedTrade = createCachedTradeDto(tradeEventDto);
+ previousTradeMap.put(tradeEventDto.getTicker(), cachedTrade);
}
- // RealTimeDataDto 객체 생성 및 반환
+ }
+
+ // 캐시용 TradeEventDto 생성 (복사본)
+ TradeEventDto createCachedTradeDto(TradeEventDto cashDataDto) {
+ return new TradeEventDto(
+ cashDataDto.getTicker(),
+ cashDataDto.getSize(),
+ cashDataDto.getPrice(),
+ cashDataDto.getTimestamp()
+ );
+ }
+
+ // RealTimeDataDto 생성
+ RealTimeDataDto createRealTimeDataDto(TradeInfo tradeInfo, double changeRate) {
return new RealTimeDataDto(
- ticker,
- currentSize,
- currentPrice,
+ tradeInfo.ticker(),
+ tradeInfo.size(),
+ tradeInfo.price(),
changeRate,
- currentTime,
- UUID.randomUUID().toString()
+ tradeInfo.timestamp(),
+ generateTransactionId()
);
}
+ // 트랜잭션 ID 생성
+ String generateTransactionId() {
+ return UUID.randomUUID().toString();
+ }
+
+ // 변동률 계산
+ public double getChangeRate(double currentPrice, double previousPrice) {
+ return ((currentPrice - previousPrice) / previousPrice) * 100;
+ }
+
+ // 거래 정보를 담는 record
+ record TradeInfo(String ticker, double price, double size, LocalDateTime timestamp) {}
+
+ // 변동률 계산 결과를 담는 record
+ record ChangeRateResult(double changeRate, boolean shouldUpdate) {}
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/service/WebsocketSendService.java b/src/main/java/com/cleanengine/coin/chart/service/WebsocketSendService.java
new file mode 100644
index 00000000..0ef1ff8d
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/chart/service/WebsocketSendService.java
@@ -0,0 +1,52 @@
+package com.cleanengine.coin.chart.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class WebsocketSendService {
+ private final SimpMessagingTemplate messagingTemplate;
+
+ //직전 데이터등락율 보내는 메세지 형식
+ public void sendChangeRate(Object data, String ticker) {
+ log.debug("티커 {} 실시간 구독 요청", ticker);
+
+ String topic = buildTopic("realTimeTradeRate", ticker);
+ sendMessage(topic, data);
+
+ log.debug("전송 완료: {} -> {}", topic, data);
+ }
+
+ //전날 종가 변동률로 보내는 메세지 형식
+ public void sendPrevRate(Object data, String ticker) {
+ log.debug("티커 {} 전일 대비 변동률 보내는 요청", ticker);
+
+ String topic = buildTopic("prevRate", ticker); // 기존 로직 유지
+ sendMessage(topic, data);
+
+ log.debug("전송 완료: {} -> {}", topic, data);
+ }
+
+ // 테스트하기 좋게 분리된 메서드들
+ String buildTopic(String topicType, String ticker) {
+ if (ticker == null || ticker.trim().isEmpty()) {
+ throw new IllegalArgumentException("티커는 비어있을 수 없습니다");
+ }
+ return "/topic/" + topicType + "/" + ticker;
+ }
+
+ void sendMessage(String topic, Object data) {
+ if (topic == null || topic.trim().isEmpty()) {
+ throw new IllegalArgumentException("토픽은 비어있을 수 없습니다");
+ }
+ if (data == null) {
+ throw new IllegalArgumentException("데이터는 null일 수 없습니다");
+ }
+
+ messagingTemplate.convertAndSend(topic, data);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java b/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java
index 5febc78b..5d1d20e6 100644
--- a/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java
+++ b/src/main/java/com/cleanengine/coin/chart/service/minute/MinuteOhlcDataServiceImpl.java
@@ -4,11 +4,12 @@
import com.cleanengine.coin.chart.repository.MinuteOhlcDataRepository;
import com.cleanengine.coin.trade.entity.Trade;
import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
-import java.util.LinkedHashMap;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -21,49 +22,98 @@ public class MinuteOhlcDataServiceImpl implements MinuteOhlcDataService {
@Override
public List getMinuteOhlcData(String ticker) {
- // 1) 해당 티커의 모든 트레이드를 시간 순으로 조회
- List trades = tradeRepository.findByTickerOrderByTradeTimeAsc(ticker);
+ validateTicker(ticker);
+
+ List trades = getTradeData(ticker);
+
+ if (trades.isEmpty()) {
+ return List.of();
+ }
+
+ Map> groupedByMinute = groupTradesByMinute(trades);
+
+ return convertToOhlcData(ticker, groupedByMinute);
+ }
- // 2) 분 단위로 그룹핑 (tradeTime 을 분 단위로 자르고 순서 유지)
- Map> byMinute = trades.stream()
+ // 입력 검증
+ void validateTicker(String ticker) {
+ if (ticker == null || ticker.trim().isEmpty()) {
+ throw new IllegalArgumentException("티커는 비어있을 수 없습니다");
+ }
+ }
+
+ // 거래 데이터 조회
+ List getTradeData(String ticker) {
+ return tradeRepository.findByTickerOrderByTradeTimeAsc(ticker);
+ }
+
+ // 분 단위 그룹핑 로직
+ Map> groupTradesByMinute(List trades) {
+ return trades.stream()
.collect(Collectors.groupingBy(
- t -> t.getTradeTime().truncatedTo(ChronoUnit.MINUTES),
+ this::truncateToMinute,
LinkedHashMap::new,
Collectors.toList()
));
+ }
+
+
+ LocalDateTime truncateToMinute(Trade trade) {
+ return trade.getTradeTime().truncatedTo(ChronoUnit.MINUTES);
+ }
- // 3) 각 분 그룹마다 OHLC + **거래량(volume)** 계산
- return byMinute.entrySet().stream()
- .map(entry -> {
- LocalDateTime minute = entry.getKey();
- List bucket = entry.getValue();
-
- double open = bucket.get(0).getPrice();
- double close = bucket.get(bucket.size() - 1).getPrice();
- double high = bucket.stream()
- .mapToDouble(Trade::getPrice)
- .max()
- .orElse(open);
- double low = bucket.stream()
- .mapToDouble(Trade::getPrice)
- .min()
- .orElse(open);
-
- // ← 여기를 바꿔서 “거래량”을 size 필드의 합으로 계산
- double volume = bucket.stream()
- .mapToDouble(Trade::getSize)
- .sum();
-
- return new RealTimeOhlcDto(
- ticker,
- minute,
- open,
- high,
- low,
- close,
- volume
- );
- })
+ // OHLC 데이터 변환 (메인 비즈니스 로직)
+ List convertToOhlcData(String ticker, Map> groupedByMinute) {
+ return groupedByMinute.entrySet().stream()
+ .map(entry -> createOhlcDto(ticker, entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
+
+ RealTimeOhlcDto createOhlcDto(String ticker, LocalDateTime minute, List trades) {
+ validateTradeList(trades);
+
+ OhlcData ohlcData = calculateOhlcData(trades);
+
+ return new RealTimeOhlcDto(
+ ticker,
+ minute,
+ ohlcData.open(),
+ ohlcData.high(),
+ ohlcData.low(),
+ ohlcData.close(),
+ ohlcData.volume()
+ );
+ }
+
+ void validateTradeList(List trades) {
+ if (trades == null || trades.isEmpty()) {
+ throw new IllegalArgumentException("거래 데이터가 없습니다");
+ }
+ }
+
+ // OHLC 계산 로직
+ @NotNull
+ static OhlcData calculateOhlcData(List trades) {
+ double open = trades.get(0).getPrice();
+ double close = trades.get(trades.size() - 1).getPrice();
+
+ double high = trades.stream()
+ .mapToDouble(Trade::getPrice)
+ .max()
+ .orElse(open);
+
+ double low = trades.stream()
+ .mapToDouble(Trade::getPrice)
+ .min()
+ .orElse(open);
+
+ double volume = trades.stream()
+ .mapToDouble(Trade::getSize)
+ .sum();
+
+ return new OhlcData(open, high, low, close, volume);
+ }
+
+ // OHLC 데이터를 위한 레코드 (불변 객체)
+ record OhlcData(double open, double high, double low, double close, double volume) {}
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/common/CommonValues.java b/src/main/java/com/cleanengine/coin/common/CommonValues.java
index d28bb807..296c4c52 100644
--- a/src/main/java/com/cleanengine/coin/common/CommonValues.java
+++ b/src/main/java/com/cleanengine/coin/common/CommonValues.java
@@ -4,6 +4,7 @@ public abstract class CommonValues {
public static final double MINIMUM_ORDER_SIZE = 0.00000001;
public static final int SELL_ORDER_BOT_ID = 1;
public static final int BUY_ORDER_BOT_ID = 2;
+ public static final double INITIAL_USER_CASH = 50_000_000.0;
public static boolean approxEquals(double a, double b) {
return Math.abs(a - b) < MINIMUM_ORDER_SIZE;
diff --git a/src/main/java/com/cleanengine/coin/common/adapter/out/store/InMemoryKeyValueStore.java b/src/main/java/com/cleanengine/coin/common/adapter/out/store/InMemoryKeyValueStore.java
new file mode 100644
index 00000000..dc71a144
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/common/adapter/out/store/InMemoryKeyValueStore.java
@@ -0,0 +1,46 @@
+package com.cleanengine.coin.common.adapter.out.store;
+
+import com.cleanengine.coin.common.domain.port.KeyValueStore;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class InMemoryKeyValueStore implements KeyValueStore {
+ private final Map map = new ConcurrentHashMap<>();
+
+ @Override
+ public void put(K key, V value) {
+ map.put(key, value);
+ }
+
+ @Override
+ public Optional get(K key) {
+ return Optional.ofNullable(map.get(key));
+ }
+
+ @Override
+ public Optional remove(K key) {
+ return Optional.ofNullable(map.remove(key));
+ }
+
+ @Override
+ public boolean isExist(K key) {
+ return map.containsKey(key);
+ }
+
+ @Override
+ public void clear() {
+ map.clear();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ @Override
+ public long size() {
+ return map.size();
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/common/adapter/out/store/InMemoryPriorityQueueStore.java b/src/main/java/com/cleanengine/coin/common/adapter/out/store/InMemoryPriorityQueueStore.java
new file mode 100644
index 00000000..fab21a92
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/common/adapter/out/store/InMemoryPriorityQueueStore.java
@@ -0,0 +1,47 @@
+package com.cleanengine.coin.common.adapter.out.store;
+
+import com.cleanengine.coin.common.domain.port.PriorityQueueStore;
+
+import java.util.concurrent.PriorityBlockingQueue;
+
+public class InMemoryPriorityQueueStore> implements PriorityQueueStore {
+ private final PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
+
+ @Override
+ public void put(T item) {
+ if(item == null) throw new IllegalArgumentException("item cannot be null.");
+ queue.add(item);
+ }
+
+ @Override
+ public T poll() {
+ return queue.poll();
+ }
+
+ @Override
+ public T peek() {
+ return queue.peek();
+ }
+
+ @Override
+ public T remove(T item) {
+ if(item == null) throw new IllegalArgumentException("item cannot be null.");
+ queue.remove(item);
+ return item;
+ }
+
+ @Override
+ public long size() {
+ return queue.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return queue.isEmpty();
+ }
+
+ @Override
+ public void clear() {
+ queue.clear();
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/common/domain/port/KeyValueStore.java b/src/main/java/com/cleanengine/coin/common/domain/port/KeyValueStore.java
new file mode 100644
index 00000000..5f2c9d05
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/common/domain/port/KeyValueStore.java
@@ -0,0 +1,19 @@
+package com.cleanengine.coin.common.domain.port;
+
+import java.util.Optional;
+
+public interface KeyValueStore {
+ void put(K key, V value);
+
+ Optional get(K key);
+
+ Optional remove(K key);
+
+ boolean isExist(K key);
+
+ void clear();
+
+ boolean isEmpty();
+
+ long size();
+}
diff --git a/src/main/java/com/cleanengine/coin/common/domain/port/PriorityQueueStore.java b/src/main/java/com/cleanengine/coin/common/domain/port/PriorityQueueStore.java
new file mode 100644
index 00000000..34f94e85
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/common/domain/port/PriorityQueueStore.java
@@ -0,0 +1,17 @@
+package com.cleanengine.coin.common.domain.port;
+
+public interface PriorityQueueStore > {
+ void put(T item);
+
+ T poll();
+
+ T peek();
+
+ T remove(T item);
+
+ long size();
+
+ boolean isEmpty();
+
+ void clear();
+}
diff --git a/src/main/java/com/cleanengine/coin/common/error/BaseExceptionHandler.java b/src/main/java/com/cleanengine/coin/common/error/BaseExceptionHandler.java
index 1b729273..6d7f8b57 100644
--- a/src/main/java/com/cleanengine/coin/common/error/BaseExceptionHandler.java
+++ b/src/main/java/com/cleanengine/coin/common/error/BaseExceptionHandler.java
@@ -3,18 +3,22 @@
import com.cleanengine.coin.common.response.ApiResponse;
import com.cleanengine.coin.common.response.ErrorResponse;
import com.cleanengine.coin.common.response.ErrorStatus;
+import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
+import org.springframework.web.servlet.resource.NoResourceFoundException;
-import java.util.Arrays;
+import java.time.LocalDateTime;
import java.util.List;
@RestControllerAdvice
@@ -54,6 +58,24 @@ protected ResponseEntity> handleHttpRequestMethodNotSupporte
return ApiResponse.fail(response).toResponseEntity();
}
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ protected ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
+ final ErrorResponse response = ErrorResponse.of(ErrorStatus.INVALID_INPUT_FORMAT);
+ return ApiResponse.fail(response).toResponseEntity();
+ }
+
+ @ExceptionHandler(NoResourceFoundException.class)
+ protected ResponseEntity> handleNoStaticResourceFoundException(NoResourceFoundException e) {
+ final ErrorResponse response = ErrorResponse.of(ErrorStatus.API_ENDPOINT_NOT_EXIST);
+ return ApiResponse.fail(response).toResponseEntity();
+ }
+
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ protected ResponseEntity> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
+ final ErrorResponse response = ErrorResponse.of(ErrorStatus.INVALID_INPUT_VALUE);
+ return ApiResponse.fail(response).toResponseEntity();
+ }
+
@ExceptionHandler(DomainValidationException.class)
protected ResponseEntity> handleDomainValidationException(DomainValidationException e) {
final ErrorResponse response = ErrorResponse.of(e.getErrorStatus(), e.getFieldErrors());
@@ -61,10 +83,16 @@ protected ResponseEntity> handleDomainValidationException(Do
}
@ExceptionHandler(Exception.class)
- protected ResponseEntity> handleException(Exception e) {
+ protected ResponseEntity> handleException(Exception e, HttpServletRequest request) {
final ErrorResponse response = ErrorResponse.of(ErrorStatus.INTERNAL_SERVER_ERROR);
- log.warn("Handling 되지 않는 에러 발생" + e.getMessage());
- log.warn("Handling 되지 않는 에러 발생" + Arrays.toString(e.getStackTrace()));
+ StringBuilder logMessage = new StringBuilder();
+ logMessage.append("=======API 요청 중 Handling 되지 않는 에러 발생=======");
+ logMessage.append("\n발생 시간 : ").append(LocalDateTime.now());
+ logMessage.append("\n요청 URI : ").append(request.getRequestURI());
+ logMessage.append("\n예외 타입 : ").append(e.getClass().getName());
+ logMessage.append("\n메시지 : ").append(e.getMessage());
+ log.warn("{}", logMessage, e);
+
return ApiResponse.fail(response).toResponseEntity();
}
}
diff --git a/src/main/java/com/cleanengine/coin/common/error/WebSocketExceptionHandler.java b/src/main/java/com/cleanengine/coin/common/error/WebSocketExceptionHandler.java
index 22fd198f..9d754ca1 100644
--- a/src/main/java/com/cleanengine/coin/common/error/WebSocketExceptionHandler.java
+++ b/src/main/java/com/cleanengine/coin/common/error/WebSocketExceptionHandler.java
@@ -1,10 +1,15 @@
package com.cleanengine.coin.common.error;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
+import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.web.bind.annotation.ControllerAdvice;
+import java.time.LocalDateTime;
+
@ControllerAdvice
+@Slf4j
public class WebSocketExceptionHandler {
@MessageExceptionHandler(DomainValidationException.class)
@@ -12,4 +17,15 @@ public class WebSocketExceptionHandler {
public String handleDomainValidationException(DomainValidationException e) {
return e.getMessage();
}
+
+ @MessageExceptionHandler(Exception.class)
+ public void handleException(Exception e, @Payload String payload) {
+ StringBuilder logMessage = new StringBuilder();
+ logMessage.append("=======Websocket 요청 중 Handling 되지 않는 에러 발생=======");
+ logMessage.append("\n발생 시간 : ").append(LocalDateTime.now());
+ logMessage.append("\n요청 내용 : ").append(payload);
+ logMessage.append("\n예외 타입 : ").append(e.getClass().getName());
+ logMessage.append("\n메시지 : ").append(e.getMessage());
+ log.warn("{}", logMessage, e);
+ }
}
diff --git a/src/main/java/com/cleanengine/coin/common/response/ErrorStatus.java b/src/main/java/com/cleanengine/coin/common/response/ErrorStatus.java
index 54ce216d..8c58aae3 100644
--- a/src/main/java/com/cleanengine/coin/common/response/ErrorStatus.java
+++ b/src/main/java/com/cleanengine/coin/common/response/ErrorStatus.java
@@ -11,7 +11,8 @@ public enum ErrorStatus {
INVALID_TYPE("A02", HttpStatus.BAD_REQUEST, "Invalid Type entered"),
INVALID_INPUT_VALUE("A03", HttpStatus.BAD_REQUEST, "Invalid Input entered"),
API_ENDPOINT_NOT_EXIST("A04", HttpStatus.NOT_FOUND, "API Endpoint doesn't exist"),
- UNAUTHORIZED_RESOURCE("A05", HttpStatus.BAD_REQUEST, "Current user can't access this resource"),
+ UNAUTHORIZED_RESOURCE("A05", HttpStatus.UNAUTHORIZED, "Current user can't access this resource"),
+ INVALID_INPUT_FORMAT("A06", HttpStatus.BAD_REQUEST, "Invalid Input Format"),
INTERNAL_SERVER_ERROR("A88", HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected Server Error occurred. Contact Backend Admin."),
NOT_CLASSIFIED_BUSINESS_ERROR("A99", HttpStatus.BAD_REQUEST, "Unclassified Business Error.");
diff --git a/src/main/java/com/cleanengine/coin/configuration/SecurityConfig.java b/src/main/java/com/cleanengine/coin/configuration/SecurityConfig.java
index b77a4547..51e8265c 100644
--- a/src/main/java/com/cleanengine/coin/configuration/SecurityConfig.java
+++ b/src/main/java/com/cleanengine/coin/configuration/SecurityConfig.java
@@ -50,7 +50,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.cors(corsCustomizer -> corsCustomizer.configurationSource(request -> {
CorsConfiguration configuration = new CorsConfiguration();
- configuration.setAllowedOrigins(Collections.singletonList(frontendBaseUrl));
+ configuration.setAllowedOrigins(List.of(frontendBaseUrl,"http://localhost:63343"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
diff --git a/src/main/java/com/cleanengine/coin/configuration/WebSocketConfig.java b/src/main/java/com/cleanengine/coin/configuration/WebSocketConfig.java
index d371e5fc..8c34c4bd 100644
--- a/src/main/java/com/cleanengine/coin/configuration/WebSocketConfig.java
+++ b/src/main/java/com/cleanengine/coin/configuration/WebSocketConfig.java
@@ -1,26 +1,24 @@
package com.cleanengine.coin.configuration;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
-import com.cleanengine.coin.configuration.SecurityEndpoints.EndpointConfig;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
- private final EndpointConfig endpointConfig;
-
- @Value("${spring.security.allowed-origins}")
- private String[] allowedOrigins;
-
- public WebSocketConfig(EndpointConfig endpointConfig) {
- this.endpointConfig = endpointConfig;
- }
+ private static final String[] ALLOWED_ORIGINS = {
+ "http://localhost:63342",
+ "http://localhost:63343",
+ "http://localhost:8080",
+ "http://localhost:5500",
+ "http://localhost:5173",
+ "https://investfuture.my"
+ };
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
@@ -30,14 +28,13 @@ public void configureMessageBroker(MessageBrokerRegistry config) {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
- endpointConfig.getWebsocketEndpoints()
- .forEach(endpoint -> registerEndpoint(registry, endpoint));
-
+ registerEndpoint(registry, "/api/coin/min");
+ registerEndpoint(registry, "/api/coin/orderbook");
}
private void registerEndpoint(StompEndpointRegistry registry, String endpoint) {
registry.addEndpoint(endpoint)
- .setAllowedOrigins(allowedOrigins);
+ .setAllowedOrigins(ALLOWED_ORIGINS);
}
}
diff --git a/src/main/java/com/cleanengine/coin/configuration/bootstrap/DBInitRunner.java b/src/main/java/com/cleanengine/coin/configuration/bootstrap/DBInitRunner.java
index e23d08f2..7177eb7d 100644
--- a/src/main/java/com/cleanengine/coin/configuration/bootstrap/DBInitRunner.java
+++ b/src/main/java/com/cleanengine/coin/configuration/bootstrap/DBInitRunner.java
@@ -1,8 +1,8 @@
package com.cleanengine.coin.configuration.bootstrap;
import com.cleanengine.coin.order.domain.Asset;
-import com.cleanengine.coin.order.external.adapter.wallet.WalletExternalRepository;
-import com.cleanengine.coin.order.infra.AssetRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.wallet.WalletExternalRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository;
import com.cleanengine.coin.user.domain.Account;
import com.cleanengine.coin.user.domain.User;
import com.cleanengine.coin.user.domain.Wallet;
@@ -18,7 +18,7 @@
import java.util.List;
@Component
-@Profile({"dev & !it"})
+@Profile({"(dev & !it & !mariadb-local) | h2-mem"})
@Order(1)
@RequiredArgsConstructor
public class DBInitRunner implements CommandLineRunner {
@@ -30,9 +30,13 @@ public class DBInitRunner implements CommandLineRunner {
@Transactional
@Override
public void run(String... args) throws Exception {
- initSellBotData();
- initBuyBotData();
- initAssetData();
+ if(userRepository.count() == 0){
+ initSellBotData();
+ initBuyBotData();
+ }
+ if(assetRepository.count() == 0){
+ initAssetData();
+ }
}
@Transactional
@@ -83,7 +87,7 @@ protected void initBuyBotData() {
protected void initAssetData() {
assetRepository.saveAll(List.of(
new Asset("BTC", "비트코인"),
- new Asset("TRUMP", "트럼프")
+ new Asset("TRUMP", "오피셜 트럼프")
));
}
}
diff --git a/src/main/java/com/cleanengine/coin/configuration/bootstrap/IconInitializer.java b/src/main/java/com/cleanengine/coin/configuration/bootstrap/IconInitializer.java
new file mode 100644
index 00000000..e4387640
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/configuration/bootstrap/IconInitializer.java
@@ -0,0 +1,76 @@
+package com.cleanengine.coin.configuration.bootstrap;
+
+import com.cleanengine.coin.common.annotation.WorkingServerProfile;
+import com.cleanengine.coin.order.domain.Asset;
+import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.stereotype.Component;
+import org.springframework.util.FileCopyUtils;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+
+@Component
+@Order(2)
+@WorkingServerProfile
+@Slf4j
+@RequiredArgsConstructor
+public class IconInitializer implements ApplicationRunner {
+ private final AssetRepository assetRepository;
+ private final ResourceLoader resourceLoader;
+
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ List assets = loadAssets();
+
+ for(Asset asset : assets){
+ if(asset.getIcon() != null) continue;
+ byte[] encodedIconBytes = loadEncodedIcon(asset.getTicker());
+ asset.setIcon(encodedIconBytes);
+ assetRepository.save(asset);
+ }
+ }
+
+ private List loadAssets(){
+ return assetRepository.findAll();
+ }
+
+ public byte[] loadEncodedIcon(String ticker){
+ Resource resource = getResource(ticker);
+ String svgContent = convertToStr(resource);
+ String base64Str = convertToBase64(svgContent);
+ return base64Str.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private String convertToStr(Resource resource){
+ String svgContent;
+ try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
+ svgContent = FileCopyUtils.copyToString(reader);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return svgContent;
+ }
+
+ private Resource getResource(String ticker){
+ Resource resource = resourceLoader.getResource("classpath:icons/" + ticker + ".svg");
+ if(!resource.exists()){
+ log.debug("Icon not found. with " + ticker);
+ }
+ return resource;
+ }
+
+ private String convertToBase64(String svgStr){
+ return Base64.getEncoder().encodeToString(svgStr.getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/in/event/SpringOrderCreatedAddQueueHandler.java b/src/main/java/com/cleanengine/coin/order/adapter/in/event/SpringOrderCreatedAddQueueHandler.java
new file mode 100644
index 00000000..4af03dbb
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/event/SpringOrderCreatedAddQueueHandler.java
@@ -0,0 +1,26 @@
+package com.cleanengine.coin.order.adapter.in.event;
+
+import com.cleanengine.coin.order.application.event.OrderCreated;
+import com.cleanengine.coin.order.application.event.OrderInsertedToQueue;
+import com.cleanengine.coin.order.domain.Order;
+import com.cleanengine.coin.order.domain.spi.WaitingOrders;
+import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+@Component
+@RequiredArgsConstructor
+public class SpringOrderCreatedAddQueueHandler {
+ private final WaitingOrdersManager waitingOrdersManager;
+ private final ApplicationEventPublisher applicationEventPublisher;
+
+ @TransactionalEventListener(OrderCreated.class)
+ public void handleOrderCreated(OrderCreated event) {
+ Order order = event.order();
+ WaitingOrders waitingOrders = waitingOrdersManager.getWaitingOrders(order.getTicker());
+ waitingOrders.addOrder(order);
+ applicationEventPublisher.publishEvent(new OrderInsertedToQueue(order));
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/in/event/SpringOrderCreatedUpdateOrderBookHandler.java b/src/main/java/com/cleanengine/coin/order/adapter/in/event/SpringOrderCreatedUpdateOrderBookHandler.java
new file mode 100644
index 00000000..ba4572e9
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/event/SpringOrderCreatedUpdateOrderBookHandler.java
@@ -0,0 +1,18 @@
+package com.cleanengine.coin.order.adapter.in.event;
+
+import com.cleanengine.coin.order.application.event.OrderCreated;
+import com.cleanengine.coin.orderbook.application.service.UpdateOrderBookUsecase;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+@Component
+@RequiredArgsConstructor
+public class SpringOrderCreatedUpdateOrderBookHandler {
+ private final UpdateOrderBookUsecase updateOrderBookUsecase;
+
+ @TransactionalEventListener(OrderCreated.class)
+ public void handleOrderCreated(OrderCreated event) {
+ updateOrderBookUsecase.updateOrderBookOnNewOrder(event.order());
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/presentation/AssetController.java b/src/main/java/com/cleanengine/coin/order/adapter/in/web/asset/AssetController.java
similarity index 92%
rename from src/main/java/com/cleanengine/coin/order/presentation/AssetController.java
rename to src/main/java/com/cleanengine/coin/order/adapter/in/web/asset/AssetController.java
index 1f4ae573..4c1cf89c 100644
--- a/src/main/java/com/cleanengine/coin/order/presentation/AssetController.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/web/asset/AssetController.java
@@ -1,7 +1,7 @@
-package com.cleanengine.coin.order.presentation;
+package com.cleanengine.coin.order.adapter.in.web.asset;
import com.cleanengine.coin.common.response.ApiResponse;
-import com.cleanengine.coin.order.application.AssetInfo;
+import com.cleanengine.coin.order.application.dto.AssetInfo;
import com.cleanengine.coin.order.application.AssetService;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/com/cleanengine/coin/order/presentation/OrderController.java b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderController.java
similarity index 89%
rename from src/main/java/com/cleanengine/coin/order/presentation/OrderController.java
rename to src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderController.java
index a9343098..9d5f1add 100644
--- a/src/main/java/com/cleanengine/coin/order/presentation/OrderController.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderController.java
@@ -1,9 +1,9 @@
-package com.cleanengine.coin.order.presentation;
+package com.cleanengine.coin.order.adapter.in.web.order;
import com.cleanengine.coin.common.response.ApiResponse;
-import com.cleanengine.coin.order.application.OrderCommand;
-import com.cleanengine.coin.order.application.OrderInfo;
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.user.login.infra.CustomOAuth2User;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/com/cleanengine/coin/order/presentation/OrderRequestDto.java b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderRequestDto.java
similarity index 78%
rename from src/main/java/com/cleanengine/coin/order/presentation/OrderRequestDto.java
rename to src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderRequestDto.java
index 0215e495..4b017cc9 100644
--- a/src/main/java/com/cleanengine/coin/order/presentation/OrderRequestDto.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderRequestDto.java
@@ -1,8 +1,8 @@
-package com.cleanengine.coin.order.presentation;
+package com.cleanengine.coin.order.adapter.in.web.order;
-import com.cleanengine.coin.order.application.OrderCommand;
-import com.cleanengine.coin.order.presentation.validation.OrderType;
-import com.cleanengine.coin.order.presentation.validation.Side;
+import com.cleanengine.coin.order.adapter.in.web.order.validation.OrderType;
+import com.cleanengine.coin.order.adapter.in.web.order.validation.Side;
+import com.cleanengine.coin.order.application.dto.OrderCommand;
import com.fasterxml.jackson.annotation.JsonCreator;
import java.time.LocalDateTime;
diff --git a/src/main/java/com/cleanengine/coin/order/presentation/OrderResponseDto.java b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderResponseDto.java
similarity index 95%
rename from src/main/java/com/cleanengine/coin/order/presentation/OrderResponseDto.java
rename to src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderResponseDto.java
index 610484f3..ef316c83 100644
--- a/src/main/java/com/cleanengine/coin/order/presentation/OrderResponseDto.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/OrderResponseDto.java
@@ -1,6 +1,6 @@
-package com.cleanengine.coin.order.presentation;
+package com.cleanengine.coin.order.adapter.in.web.order;
-import com.cleanengine.coin.order.application.OrderInfo;
+import com.cleanengine.coin.order.application.dto.OrderInfo;
import com.cleanengine.coin.order.domain.OrderStatus;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
diff --git a/src/main/java/com/cleanengine/coin/order/presentation/validation/OrderType.java b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/OrderType.java
similarity index 88%
rename from src/main/java/com/cleanengine/coin/order/presentation/validation/OrderType.java
rename to src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/OrderType.java
index 5d9e9e75..93a961c4 100644
--- a/src/main/java/com/cleanengine/coin/order/presentation/validation/OrderType.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/OrderType.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.presentation.validation;
+package com.cleanengine.coin.order.adapter.in.web.order.validation;
import jakarta.validation.Constraint;
diff --git a/src/main/java/com/cleanengine/coin/order/presentation/validation/OrderTypeValidator.java b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/OrderTypeValidator.java
similarity index 90%
rename from src/main/java/com/cleanengine/coin/order/presentation/validation/OrderTypeValidator.java
rename to src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/OrderTypeValidator.java
index af2bc244..4a4cc056 100644
--- a/src/main/java/com/cleanengine/coin/order/presentation/validation/OrderTypeValidator.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/OrderTypeValidator.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.presentation.validation;
+package com.cleanengine.coin.order.adapter.in.web.order.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
diff --git a/src/main/java/com/cleanengine/coin/order/presentation/validation/Side.java b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/Side.java
similarity index 88%
rename from src/main/java/com/cleanengine/coin/order/presentation/validation/Side.java
rename to src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/Side.java
index 0544fd2b..6895d90a 100644
--- a/src/main/java/com/cleanengine/coin/order/presentation/validation/Side.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/Side.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.presentation.validation;
+package com.cleanengine.coin.order.adapter.in.web.order.validation;
import jakarta.validation.Constraint;
diff --git a/src/main/java/com/cleanengine/coin/order/presentation/validation/SideValidator.java b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/SideValidator.java
similarity index 86%
rename from src/main/java/com/cleanengine/coin/order/presentation/validation/SideValidator.java
rename to src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/SideValidator.java
index 3cd6c771..2cfcfa23 100644
--- a/src/main/java/com/cleanengine/coin/order/presentation/validation/SideValidator.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/in/web/order/validation/SideValidator.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.presentation.validation;
+package com.cleanengine.coin.order.adapter.in.web.order.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/event/SpringOrderCreatedPublisher.java b/src/main/java/com/cleanengine/coin/order/adapter/out/event/SpringOrderCreatedPublisher.java
new file mode 100644
index 00000000..554c1a77
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/event/SpringOrderCreatedPublisher.java
@@ -0,0 +1,18 @@
+package com.cleanengine.coin.order.adapter.out.event;
+
+import com.cleanengine.coin.order.application.event.OrderCreated;
+import com.cleanengine.coin.order.application.port.out.PublishOrderCreatedPort;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class SpringOrderCreatedPublisher implements PublishOrderCreatedPort {
+ private final ApplicationEventPublisher applicationEventPublisher;
+
+ @Override
+ public void publish(OrderCreated orderCreated) {
+ applicationEventPublisher.publishEvent(orderCreated);
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/external/adapter/account/AccountExternalRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/AccountExternalRepository.java
similarity index 81%
rename from src/main/java/com/cleanengine/coin/order/external/adapter/account/AccountExternalRepository.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/AccountExternalRepository.java
index c4f25c0a..d5e70575 100644
--- a/src/main/java/com/cleanengine/coin/order/external/adapter/account/AccountExternalRepository.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/AccountExternalRepository.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.external.adapter.account;
+package com.cleanengine.coin.order.adapter.out.persistentce.account;
import com.cleanengine.coin.user.domain.Account;
import org.springframework.data.repository.CrudRepository;
diff --git a/src/main/java/com/cleanengine/coin/order/external/adapter/account/AccountExternalService.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/AccountExternalService.java
similarity index 91%
rename from src/main/java/com/cleanengine/coin/order/external/adapter/account/AccountExternalService.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/AccountExternalService.java
index 45ebfc76..d70869ac 100644
--- a/src/main/java/com/cleanengine/coin/order/external/adapter/account/AccountExternalService.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/AccountExternalService.java
@@ -1,7 +1,7 @@
-package com.cleanengine.coin.order.external.adapter.account;
+package com.cleanengine.coin.order.adapter.out.persistentce.account;
import com.cleanengine.coin.common.error.DomainValidationException;
-import com.cleanengine.coin.order.application.port.AccountUpdatePort;
+import com.cleanengine.coin.order.application.port.out.AccountUpdatePort;
import com.cleanengine.coin.user.domain.Account;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/cleanengine/coin/order/external/adapter/account/LockDepositService.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/LockDepositService.java
similarity index 91%
rename from src/main/java/com/cleanengine/coin/order/external/adapter/account/LockDepositService.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/LockDepositService.java
index 3b23d3ec..1e83ed90 100644
--- a/src/main/java/com/cleanengine/coin/order/external/adapter/account/LockDepositService.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/account/LockDepositService.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.external.adapter.account;
+package com.cleanengine.coin.order.adapter.out.persistentce.account;
import com.cleanengine.coin.common.error.DomainValidationException;
import com.cleanengine.coin.user.domain.Account;
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetCacheRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetCacheRepository.java
new file mode 100644
index 00000000..3c1f3665
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetCacheRepository.java
@@ -0,0 +1,26 @@
+package com.cleanengine.coin.order.adapter.out.persistentce.asset;
+
+import com.cleanengine.coin.order.domain.Asset;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class AssetCacheRepository {
+ private final ConcurrentHashMap assetCache = new ConcurrentHashMap<>();
+
+ public synchronized void saveAsset(Asset asset) {
+ if(!assetCache.containsKey(asset.getTicker())){
+ assetCache.put(asset.getTicker(), asset);
+ }
+ }
+
+ public Optional getAsset(String ticker){
+ return Optional.ofNullable(assetCache.get(ticker));
+ }
+
+ public boolean isAssetExists(String ticker){
+ return assetCache.containsKey(ticker);
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/infra/AssetRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetRepository.java
similarity index 58%
rename from src/main/java/com/cleanengine/coin/order/infra/AssetRepository.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetRepository.java
index 2133b64f..4115e9f7 100644
--- a/src/main/java/com/cleanengine/coin/order/infra/AssetRepository.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/asset/AssetRepository.java
@@ -1,7 +1,11 @@
-package com.cleanengine.coin.order.infra;
+package com.cleanengine.coin.order.adapter.out.persistentce.asset;
import com.cleanengine.coin.order.domain.Asset;
import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.List;
+
public interface AssetRepository extends JpaRepository {
+ @Override
+ List findAll();
}
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/InMemoryActiveOrders.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/InMemoryActiveOrders.java
new file mode 100644
index 00000000..67ae4cf8
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/InMemoryActiveOrders.java
@@ -0,0 +1,62 @@
+package com.cleanengine.coin.order.adapter.out.persistentce.order;
+
+import com.cleanengine.coin.common.adapter.out.store.InMemoryKeyValueStore;
+import com.cleanengine.coin.order.domain.BuyOrder;
+import com.cleanengine.coin.order.domain.Order;
+import com.cleanengine.coin.order.domain.SellOrder;
+import com.cleanengine.coin.order.domain.spi.ActiveOrders;
+import com.cleanengine.coin.common.domain.port.KeyValueStore;
+import lombok.Getter;
+
+import java.util.Optional;
+
+public class InMemoryActiveOrders implements ActiveOrders {
+ @Getter
+ private final String ticker;
+ private final InMemoryKeyValueStore activeBuyOrders = new InMemoryKeyValueStore<>();
+ private final InMemoryKeyValueStore activeSellOrders = new InMemoryKeyValueStore<>();
+
+ public InMemoryActiveOrders(String ticker) {
+ this.ticker = ticker;
+ }
+
+ @Override
+ public void saveOrder(Order order) {
+ if(order.getIsMarketOrder()){
+ return;
+ }
+ if(order instanceof BuyOrder){
+ activeBuyOrders.put(order.getId(), (BuyOrder) order);
+ } else {
+ activeSellOrders.put(order.getId(), (SellOrder) order);
+ }
+ }
+
+ @Override
+ public Optional getOrder(Long orderId, boolean isBuyOrder) {
+ if(isBuyOrder) {
+ return activeBuyOrders.get(orderId).map(order-> order);
+ } else {
+ return activeSellOrders.get(orderId).map(order-> order);
+ }
+ }
+
+ @Override
+ public Optional removeOrder(Long orderId, boolean isBuyOrder) {
+ if(isBuyOrder) {
+ return activeBuyOrders.remove(orderId).map(order-> order);
+ } else {
+ return activeSellOrders.remove(orderId).map(order-> order);
+ }
+ }
+
+ @Override
+ public KeyValueStore getBuyOrderKeyValueStore() {
+ return activeBuyOrders;
+ }
+
+ @Override
+ public KeyValueStore getSellOrderKeyValueStore() {
+ return activeSellOrders;
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/InMemoryActiveOrdersManager.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/InMemoryActiveOrdersManager.java
new file mode 100644
index 00000000..cb2ba131
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/InMemoryActiveOrdersManager.java
@@ -0,0 +1,61 @@
+package com.cleanengine.coin.order.adapter.out.persistentce.order;
+
+import com.cleanengine.coin.order.domain.Order;
+import com.cleanengine.coin.order.domain.spi.ActiveOrders;
+import com.cleanengine.coin.order.domain.spi.ActiveOrdersManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Optional;
+
+@Slf4j
+@Component
+public class InMemoryActiveOrdersManager implements ActiveOrdersManager {
+ private final HashMap activeOrdersMap = new HashMap<>();
+
+ public void saveOrder(String ticker, Order order) {
+ ActiveOrders activeOrders = getActiveOrders(ticker);
+ activeOrders.saveOrder(order);
+ }
+
+ public Optional getOrder(String ticker, Long orderId, boolean isBuyOrder) {
+ ActiveOrders activeOrders = getActiveOrders(ticker);
+ return activeOrders.getOrder(orderId,isBuyOrder);
+ }
+
+ public void removeOrder(String ticker, Long orderId, boolean isBuyOrder) {
+ ActiveOrders activeOrders = getActiveOrders(ticker);
+ activeOrders.removeOrder(orderId, isBuyOrder);
+ }
+
+ protected synchronized void addActiveOrderManager(String ticker) {
+ if(!activeOrdersMap.containsKey(ticker)){
+ activeOrdersMap.put(ticker, new InMemoryActiveOrders(ticker));
+ }
+ }
+
+ @Override
+ public ActiveOrders getActiveOrders(String ticker) {
+ if(!activeOrdersMap.containsKey(ticker)){
+ addActiveOrderManager(ticker);
+ }
+
+ Optional activeOrderManager = Optional.ofNullable(activeOrdersMap.get(ticker));
+ if(activeOrderManager.isEmpty()){
+ log.debug("ActiveOrderManager not found with " + ticker);
+ throw new RuntimeException("ActiveOrderManager not found with " + ticker);
+ }
+ return activeOrderManager.get();
+ }
+
+ @Override
+ public void removeActiveOrders(String ticker) {
+ activeOrdersMap.remove(ticker);
+ }
+
+ @Override
+ public void close() {
+ // TODO InMemory상 close시 처리할게 있나?
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/infra/BuyOrderRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/command/BuyOrderRepository.java
similarity index 71%
rename from src/main/java/com/cleanengine/coin/order/infra/BuyOrderRepository.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/command/BuyOrderRepository.java
index 3df02889..d3b3cd40 100644
--- a/src/main/java/com/cleanengine/coin/order/infra/BuyOrderRepository.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/command/BuyOrderRepository.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.infra;
+package com.cleanengine.coin.order.adapter.out.persistentce.order.command;
import com.cleanengine.coin.order.domain.BuyOrder;
import org.springframework.data.repository.CrudRepository;
diff --git a/src/main/java/com/cleanengine/coin/order/infra/SellOrderRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/command/SellOrderRepository.java
similarity index 72%
rename from src/main/java/com/cleanengine/coin/order/infra/SellOrderRepository.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/command/SellOrderRepository.java
index ad5dc3fc..e568308c 100644
--- a/src/main/java/com/cleanengine/coin/order/infra/SellOrderRepository.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/command/SellOrderRepository.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.infra;
+package com.cleanengine.coin.order.adapter.out.persistentce.order.command;
import com.cleanengine.coin.order.domain.SellOrder;
import org.springframework.data.repository.CrudRepository;
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/query/BuyOrderQueryRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/query/BuyOrderQueryRepository.java
new file mode 100644
index 00000000..8cb7c169
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/query/BuyOrderQueryRepository.java
@@ -0,0 +1,14 @@
+package com.cleanengine.coin.order.adapter.out.persistentce.order.query;
+
+import com.cleanengine.coin.order.domain.BuyOrder;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface BuyOrderQueryRepository extends CrudRepository {
+ @Query("select o from buy_orders o where o.state = 'WAIT'")
+ List findIncompletedBuyOrders();
+}
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/query/SellOrderQueryRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/query/SellOrderQueryRepository.java
new file mode 100644
index 00000000..f6e7e1b8
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/query/SellOrderQueryRepository.java
@@ -0,0 +1,14 @@
+package com.cleanengine.coin.order.adapter.out.persistentce.order.query;
+
+import com.cleanengine.coin.order.domain.SellOrder;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface SellOrderQueryRepository extends CrudRepository {
+ @Query("select o from sell_orders o where o.state = 'WAIT'")
+ List findIncompletedSellOrders();
+}
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrders.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrders.java
new file mode 100644
index 00000000..f56da598
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrders.java
@@ -0,0 +1,91 @@
+package com.cleanengine.coin.order.adapter.out.persistentce.order.queue;
+
+import com.cleanengine.coin.common.adapter.out.store.InMemoryPriorityQueueStore;
+import com.cleanengine.coin.common.domain.port.PriorityQueueStore;
+import com.cleanengine.coin.order.domain.BuyOrder;
+import com.cleanengine.coin.order.domain.Order;
+import com.cleanengine.coin.order.domain.OrderType;
+import com.cleanengine.coin.order.domain.SellOrder;
+import com.cleanengine.coin.order.domain.spi.WaitingOrders;
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class InMemoryWaitingOrders implements WaitingOrders {
+ private final String ticker;
+ private final InMemoryPriorityQueueStore limitBuyOrderPriorityQueueStore =
+ new InMemoryPriorityQueueStore<>();
+ private final InMemoryPriorityQueueStore marketBuyOrderPriorityQueueStore =
+ new InMemoryPriorityQueueStore<>();
+ private final InMemoryPriorityQueueStore limitSellOrderPriorityQueueStore =
+ new InMemoryPriorityQueueStore<>();
+ private final InMemoryPriorityQueueStore marketSellOrderPriorityQueueStore =
+ new InMemoryPriorityQueueStore<>();
+
+ @Override
+ public String getTicker() {
+ return ticker;
+ }
+
+ @Override
+ public void addOrder(Order order) {
+ if(order == null) throw new IllegalArgumentException("order cannot be null.");
+
+ if (order instanceof BuyOrder) {
+ if(order.getIsMarketOrder()) {
+ marketBuyOrderPriorityQueueStore.put((BuyOrder) order);
+ }
+ else{
+ limitBuyOrderPriorityQueueStore.put((BuyOrder) order);
+ }
+ } else {
+ if(order.getIsMarketOrder()) {
+ marketSellOrderPriorityQueueStore.put((SellOrder) order);
+ }
+ else{
+ limitSellOrderPriorityQueueStore.put((SellOrder) order);
+ }
+ }
+ }
+
+ public PriorityQueueStore getBuyOrderPriorityQueueStore(OrderType orderType) {
+ return orderType == OrderType.MARKET ? marketBuyOrderPriorityQueueStore : limitBuyOrderPriorityQueueStore;
+ }
+
+ public PriorityQueueStore getSellOrderPriorityQueueStore(OrderType orderType) {
+ return orderType == OrderType.MARKET ? marketSellOrderPriorityQueueStore : limitSellOrderPriorityQueueStore;
+ }
+
+ @Override
+ public void removeOrder(Order order) {
+ if(order == null) throw new IllegalArgumentException("order cannot be null.");
+
+ if (order instanceof BuyOrder) {
+ if(order.getIsMarketOrder()) {
+ marketBuyOrderPriorityQueueStore.remove((BuyOrder) order);
+ }
+ else{
+ limitBuyOrderPriorityQueueStore.remove((BuyOrder) order);
+ }
+ } else {
+ if(order.getIsMarketOrder()) {
+ marketSellOrderPriorityQueueStore.remove((SellOrder) order);
+ }
+ else{
+ limitSellOrderPriorityQueueStore.remove((SellOrder) order);
+ }
+ }
+ }
+
+ @Override
+ public void clearAllQueues() {
+ limitBuyOrderPriorityQueueStore.clear();
+ marketBuyOrderPriorityQueueStore.clear();
+ limitSellOrderPriorityQueueStore.clear();
+ marketSellOrderPriorityQueueStore.clear();
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java
new file mode 100644
index 00000000..83d29bf6
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/InMemoryWaitingOrdersManager.java
@@ -0,0 +1,45 @@
+package com.cleanengine.coin.order.adapter.out.persistentce.order.queue;
+
+import com.cleanengine.coin.order.domain.spi.WaitingOrders;
+import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Optional;
+
+@Slf4j
+@Component
+public class InMemoryWaitingOrdersManager implements WaitingOrdersManager {
+ private final HashMap waitingOrdersMap = new HashMap<>();
+
+ @Override
+ public WaitingOrders getWaitingOrders(String ticker) {
+ if(!waitingOrdersMap.containsKey(ticker)) {
+ addWaitingOrders(ticker);
+ }
+
+ Optional waitingOrdersOpt = Optional.ofNullable(waitingOrdersMap.get(ticker));
+ if(waitingOrdersOpt.isEmpty()){
+ log.debug("WaitingOrders not found. with " + ticker);
+ throw new RuntimeException("WaitingOrders not found with " + ticker);
+ }
+ return waitingOrdersOpt.get();
+ }
+
+ @Override
+ public void removeWaitingOrders(String ticker) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ protected synchronized void addWaitingOrders(String ticker){
+ if(!waitingOrdersMap.containsKey(ticker)){
+ waitingOrdersMap.put(ticker, new InMemoryWaitingOrders(ticker));
+ }
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/application/queue/OrderQueueManager.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/OrderQueueManager.java
similarity index 51%
rename from src/main/java/com/cleanengine/coin/order/application/queue/OrderQueueManager.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/OrderQueueManager.java
index d2679cb7..9d0a142c 100644
--- a/src/main/java/com/cleanengine/coin/order/application/queue/OrderQueueManager.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/OrderQueueManager.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.application.queue;
+package com.cleanengine.coin.order.adapter.out.persistentce.order.queue;
import com.cleanengine.coin.order.domain.BuyOrder;
import com.cleanengine.coin.order.domain.Order;
@@ -7,6 +7,7 @@
import java.util.concurrent.PriorityBlockingQueue;
+@Deprecated
@Getter
public class OrderQueueManager {
private final String ticker;
@@ -38,4 +39,53 @@ public void addOrder(Order order) {
}
}
}
+
+ public int getMarketSellOrderQueueSize() {
+ return marketSellOrderQueue.size();
+ }
+
+ public int getMarketBuyOrderQueueSize() {
+ return marketBuyOrderQueue.size();
+ }
+
+ public int getLimitSellOrderQueueSize() {
+ return limitSellOrderQueue.size();
+ }
+
+ public int getLimitBuyOrderQueueSize() {
+ return limitBuyOrderQueue.size();
+ }
+
+ public SellOrder getMarketSellOrder() {
+ return marketSellOrderQueue.peek();
+ }
+
+ public BuyOrder getMarketBuyOrder() {
+ return marketBuyOrderQueue.peek();
+ }
+
+ public SellOrder getLimitSellOrder() {
+ return limitSellOrderQueue.peek();
+ }
+
+ public BuyOrder getLimitBuyOrder() {
+ return limitBuyOrderQueue.peek();
+ }
+
+ public boolean removeOrderFromQueue(Order order) {
+ if (order instanceof SellOrder) {
+ if (order.getIsMarketOrder()) {
+ return marketSellOrderQueue.remove(order);
+ } else {
+ return limitSellOrderQueue.remove(order);
+ }
+ } else {
+ if (order.getIsMarketOrder()) {
+ return marketBuyOrderQueue.remove(order);
+ } else {
+ return limitBuyOrderQueue.remove(order);
+ }
+ }
+ }
+
}
diff --git a/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/OrderQueueManagerPool.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/OrderQueueManagerPool.java
new file mode 100644
index 00000000..cd394214
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/order/queue/OrderQueueManagerPool.java
@@ -0,0 +1,33 @@
+package com.cleanengine.coin.order.adapter.out.persistentce.order.queue;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Optional;
+
+@Deprecated
+@Slf4j
+@Component
+public class OrderQueueManagerPool {
+ private final HashMap orderQueueManagerMap = new HashMap<>();
+
+ public OrderQueueManager getOrderQueueManager(String ticker){
+ if(!orderQueueManagerMap.containsKey(ticker)){
+ addOrderQueueManager(ticker);
+ }
+
+ Optional orderQueueManagerOpt = Optional.ofNullable(orderQueueManagerMap.get(ticker));
+ if(orderQueueManagerOpt.isEmpty()){
+ log.debug("OrderQueueManager not found. with " + ticker);
+ throw new RuntimeException("OrderQueueManager not found with " + ticker);
+ }
+ return orderQueueManagerOpt.get();
+ }
+
+ protected synchronized void addOrderQueueManager(String ticker){
+ if(!orderQueueManagerMap.containsKey(ticker)){
+ orderQueueManagerMap.put(ticker, new OrderQueueManager(ticker));
+ }
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/LockAssetService.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/LockAssetService.java
similarity index 89%
rename from src/main/java/com/cleanengine/coin/order/external/adapter/wallet/LockAssetService.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/LockAssetService.java
index 397197f0..024d3f21 100644
--- a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/LockAssetService.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/LockAssetService.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.external.adapter.wallet;
+package com.cleanengine.coin.order.adapter.out.persistentce.wallet;
import com.cleanengine.coin.common.error.DomainValidationException;
import com.cleanengine.coin.user.domain.Wallet;
diff --git a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepository.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepository.java
similarity index 76%
rename from src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepository.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepository.java
index 922037f5..d1b5e37e 100644
--- a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepository.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepository.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.external.adapter.wallet;
+package com.cleanengine.coin.order.adapter.out.persistentce.wallet;
import com.cleanengine.coin.user.domain.Wallet;
import org.springframework.data.repository.CrudRepository;
diff --git a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepositoryCustom.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepositoryCustom.java
similarity index 74%
rename from src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepositoryCustom.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepositoryCustom.java
index 9f494b42..d6ff1ec1 100644
--- a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepositoryCustom.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepositoryCustom.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.external.adapter.wallet;
+package com.cleanengine.coin.order.adapter.out.persistentce.wallet;
import com.cleanengine.coin.user.domain.Wallet;
diff --git a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepositoryCustomImpl.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepositoryCustomImpl.java
similarity index 94%
rename from src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepositoryCustomImpl.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepositoryCustomImpl.java
index 25037342..f4968bfb 100644
--- a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalRepositoryCustomImpl.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalRepositoryCustomImpl.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.external.adapter.wallet;
+package com.cleanengine.coin.order.adapter.out.persistentce.wallet;
import com.cleanengine.coin.user.domain.Wallet;
import jakarta.persistence.EntityManager;
diff --git a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalService.java b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalService.java
similarity index 91%
rename from src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalService.java
rename to src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalService.java
index 5b018182..039fd9be 100644
--- a/src/main/java/com/cleanengine/coin/order/external/adapter/wallet/WalletExternalService.java
+++ b/src/main/java/com/cleanengine/coin/order/adapter/out/persistentce/wallet/WalletExternalService.java
@@ -1,7 +1,7 @@
-package com.cleanengine.coin.order.external.adapter.wallet;
+package com.cleanengine.coin.order.adapter.out.persistentce.wallet;
import com.cleanengine.coin.common.error.DomainValidationException;
-import com.cleanengine.coin.order.application.port.WalletUpdatePort;
+import com.cleanengine.coin.order.application.port.out.WalletUpdatePort;
import com.cleanengine.coin.user.domain.Wallet;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/cleanengine/coin/order/application/AssetInfo.java b/src/main/java/com/cleanengine/coin/order/application/AssetInfo.java
deleted file mode 100644
index 55f4d52e..00000000
--- a/src/main/java/com/cleanengine/coin/order/application/AssetInfo.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.cleanengine.coin.order.application;
-
-import com.cleanengine.coin.order.domain.Asset;
-import com.fasterxml.jackson.annotation.JsonPropertyOrder;
-
-@JsonPropertyOrder({"ticker", "name"})
-public record AssetInfo(
- String ticker,
- String name
-){
- public static AssetInfo from(Asset asset){
- return new AssetInfo(asset.getTicker(), asset.getName());
- }
-}
diff --git a/src/main/java/com/cleanengine/coin/order/application/AssetService.java b/src/main/java/com/cleanengine/coin/order/application/AssetService.java
index d44a2e04..cdea7404 100644
--- a/src/main/java/com/cleanengine/coin/order/application/AssetService.java
+++ b/src/main/java/com/cleanengine/coin/order/application/AssetService.java
@@ -1,29 +1,48 @@
package com.cleanengine.coin.order.application;
import com.cleanengine.coin.common.error.DomainValidationException;
+import com.cleanengine.coin.order.application.dto.AssetInfo;
import com.cleanengine.coin.order.domain.Asset;
-import com.cleanengine.coin.order.infra.AssetRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetCacheRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.validation.FieldError;
import java.util.List;
+import java.util.Optional;
@Service
@RequiredArgsConstructor
public class AssetService {
private final AssetRepository assetRepository;
+ private final AssetCacheRepository assetCacheRepository;
public AssetInfo getAssetInfo(String ticker){
- Asset asset = assetRepository.findById(ticker).orElseThrow(
- () -> new DomainValidationException(
- String.format("Asset %s not found", ticker),
- List.of(new FieldError("Asset", "ticker", "Asset not found")))
- );
- return AssetInfo.from(asset);
+ Optional assetOpt = getAsset(ticker);
+ if(assetOpt.isEmpty()){
+ throw new DomainValidationException(
+ String.format("Asset %s not found", ticker),
+ List.of(new FieldError("Asset", "ticker", "Asset not found")));
+ }
+
+ return AssetInfo.from(assetOpt.get());
}
public List getAllAssetInfos(){
return assetRepository.findAll().stream().map(AssetInfo::from).toList();
}
+
+ public boolean isAssetExist(String ticker){
+ if(assetCacheRepository.isAssetExists(ticker)) return true;
+
+ Optional asset = getAsset(ticker);
+ asset.ifPresent(assetCacheRepository::saveAsset);
+
+ return asset.isPresent();
+ }
+
+ protected Optional getAsset(String ticker){
+ return assetRepository.findById(ticker);
+ }
}
diff --git a/src/main/java/com/cleanengine/coin/order/application/OrderCreated.java b/src/main/java/com/cleanengine/coin/order/application/OrderCreated.java
deleted file mode 100644
index 9892643c..00000000
--- a/src/main/java/com/cleanengine/coin/order/application/OrderCreated.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.cleanengine.coin.order.application;
-
-import com.cleanengine.coin.order.domain.Order;
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class OrderCreated {
- private final Order order;
- public OrderCreated(Order order){
- this.order = order;
- }
-}
diff --git a/src/main/java/com/cleanengine/coin/order/application/OrderRestoreService.java b/src/main/java/com/cleanengine/coin/order/application/OrderRestoreService.java
new file mode 100644
index 00000000..cf28ed4b
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/application/OrderRestoreService.java
@@ -0,0 +1,43 @@
+package com.cleanengine.coin.order.application;
+
+import com.cleanengine.coin.common.annotation.WorkingServerProfile;
+import com.cleanengine.coin.order.adapter.out.persistentce.order.query.BuyOrderQueryRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.order.query.SellOrderQueryRepository;
+import com.cleanengine.coin.order.domain.BuyOrder;
+import com.cleanengine.coin.order.domain.Order;
+import com.cleanengine.coin.order.domain.SellOrder;
+import com.cleanengine.coin.order.domain.spi.WaitingOrders;
+import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager;
+import com.cleanengine.coin.orderbook.application.service.UpdateOrderBookUsecase;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@WorkingServerProfile
+@org.springframework.core.annotation.Order(3)
+@RequiredArgsConstructor
+public class OrderRestoreService implements ApplicationRunner {
+
+ private final WaitingOrdersManager waitingOrdersManager;
+ private final UpdateOrderBookUsecase updateOrderBookUsecase;
+ private final BuyOrderQueryRepository buyOrderQueryRepository;
+ private final SellOrderQueryRepository sellOrderQueryRepository;
+
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ List buyOrders = buyOrderQueryRepository.findIncompletedBuyOrders();
+ buyOrders.forEach(this::restoreOrder);
+ List sellOrders = sellOrderQueryRepository.findIncompletedSellOrders();
+ sellOrders.forEach(this::restoreOrder);
+ }
+
+ protected void restoreOrder(Order order){
+ WaitingOrders waitingOrders = waitingOrdersManager.getWaitingOrders(order.getTicker());
+ waitingOrders.addOrder(order);
+ updateOrderBookUsecase.updateOrderBookOnNewOrder(order);
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/application/OrderService.java b/src/main/java/com/cleanengine/coin/order/application/OrderService.java
index 0bf97fd5..3d723e7c 100644
--- a/src/main/java/com/cleanengine/coin/order/application/OrderService.java
+++ b/src/main/java/com/cleanengine/coin/order/application/OrderService.java
@@ -1,10 +1,10 @@
package com.cleanengine.coin.order.application;
+import com.cleanengine.coin.order.application.dto.OrderCommand;
+import com.cleanengine.coin.order.application.dto.OrderInfo;
import com.cleanengine.coin.order.application.strategy.CreateOrderStrategy;
-import com.cleanengine.coin.order.domain.Order;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
-import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@@ -19,17 +19,15 @@
@Service
@RequiredArgsConstructor
@Validated
-public class OrderService { //facade
+public class OrderService {
private final List> createOrderStrategies;
- private final ApplicationEventPublisher applicationEventPublisher;
@Transactional
public OrderInfo> createOrder(@Valid OrderCommand.CreateOrder createOrder){
CreateOrderStrategy, ?> createOrderStrategy = createOrderStrategies.stream()
.filter(strategy -> strategy.supports(createOrder.isBuyOrder())).findFirst().orElseThrow();
- Order order = createOrderStrategy.processCreatingOrder(createOrder);
- applicationEventPublisher.publishEvent(new OrderCreated(order));
- return createOrderStrategy.extractOrderInfo(order);
+
+ return createOrderStrategy.processCreatingOrder(createOrder);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
diff --git a/src/main/java/com/cleanengine/coin/order/application/dto/AssetInfo.java b/src/main/java/com/cleanengine/coin/order/application/dto/AssetInfo.java
new file mode 100644
index 00000000..9af97fe1
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/application/dto/AssetInfo.java
@@ -0,0 +1,25 @@
+package com.cleanengine.coin.order.application.dto;
+
+import com.cleanengine.coin.order.domain.Asset;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import java.nio.charset.StandardCharsets;
+
+@JsonPropertyOrder({"ticker", "name"})
+public record AssetInfo(
+ String ticker,
+ String name,
+ String svgIconBase64
+){
+ public static AssetInfo from(Asset asset){
+ byte[] iconBytes = asset.getIcon();
+
+ String iconStr = null;
+
+ if(iconBytes != null){
+ iconStr = new String(iconBytes, StandardCharsets.UTF_8);
+ }
+
+ return new AssetInfo(asset.getTicker(), asset.getName(), iconStr);
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/order/application/OrderCommand.java b/src/main/java/com/cleanengine/coin/order/application/dto/OrderCommand.java
similarity index 96%
rename from src/main/java/com/cleanengine/coin/order/application/OrderCommand.java
rename to src/main/java/com/cleanengine/coin/order/application/dto/OrderCommand.java
index 7656f125..39e11f0f 100644
--- a/src/main/java/com/cleanengine/coin/order/application/OrderCommand.java
+++ b/src/main/java/com/cleanengine/coin/order/application/dto/OrderCommand.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.application;
+package com.cleanengine.coin.order.application.dto;
import com.cleanengine.coin.common.validation.ConstraintMessageTemplate;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/cleanengine/coin/order/application/OrderInfo.java b/src/main/java/com/cleanengine/coin/order/application/dto/OrderInfo.java
similarity index 96%
rename from src/main/java/com/cleanengine/coin/order/application/OrderInfo.java
rename to src/main/java/com/cleanengine/coin/order/application/dto/OrderInfo.java
index 58cdf70d..11e996df 100644
--- a/src/main/java/com/cleanengine/coin/order/application/OrderInfo.java
+++ b/src/main/java/com/cleanengine/coin/order/application/dto/OrderInfo.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.application;
+package com.cleanengine.coin.order.application.dto;
import com.cleanengine.coin.order.domain.BuyOrder;
import com.cleanengine.coin.order.domain.Order;
diff --git a/src/main/java/com/cleanengine/coin/order/application/event/OrderCreated.java b/src/main/java/com/cleanengine/coin/order/application/event/OrderCreated.java
new file mode 100644
index 00000000..be67e15d
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/application/event/OrderCreated.java
@@ -0,0 +1,7 @@
+package com.cleanengine.coin.order.application.event;
+
+import com.cleanengine.coin.order.domain.Order;
+
+public record OrderCreated (
+ Order order
+){ }
diff --git a/src/main/java/com/cleanengine/coin/order/application/event/OrderInsertedToQueue.java b/src/main/java/com/cleanengine/coin/order/application/event/OrderInsertedToQueue.java
new file mode 100644
index 00000000..dfe5caff
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/application/event/OrderInsertedToQueue.java
@@ -0,0 +1,7 @@
+package com.cleanengine.coin.order.application.event;
+
+import com.cleanengine.coin.order.domain.Order;
+
+public record OrderInsertedToQueue (
+ Order order
+){};
diff --git a/src/main/java/com/cleanengine/coin/order/application/port/AccountUpdatePort.java b/src/main/java/com/cleanengine/coin/order/application/port/out/AccountUpdatePort.java
similarity index 70%
rename from src/main/java/com/cleanengine/coin/order/application/port/AccountUpdatePort.java
rename to src/main/java/com/cleanengine/coin/order/application/port/out/AccountUpdatePort.java
index 0c4e7b8f..8f8d40a9 100644
--- a/src/main/java/com/cleanengine/coin/order/application/port/AccountUpdatePort.java
+++ b/src/main/java/com/cleanengine/coin/order/application/port/out/AccountUpdatePort.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.application.port;
+package com.cleanengine.coin.order.application.port.out;
public interface AccountUpdatePort {
void lockDepositForBuyOrder(Integer userId, Double orderAmount) throws RuntimeException;
diff --git a/src/main/java/com/cleanengine/coin/order/application/port/out/PublishOrderCreatedPort.java b/src/main/java/com/cleanengine/coin/order/application/port/out/PublishOrderCreatedPort.java
new file mode 100644
index 00000000..6a2106b9
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/application/port/out/PublishOrderCreatedPort.java
@@ -0,0 +1,7 @@
+package com.cleanengine.coin.order.application.port.out;
+
+import com.cleanengine.coin.order.application.event.OrderCreated;
+
+public interface PublishOrderCreatedPort {
+ void publish(OrderCreated orderCreated);
+}
diff --git a/src/main/java/com/cleanengine/coin/order/application/port/WalletUpdatePort.java b/src/main/java/com/cleanengine/coin/order/application/port/out/WalletUpdatePort.java
similarity index 71%
rename from src/main/java/com/cleanengine/coin/order/application/port/WalletUpdatePort.java
rename to src/main/java/com/cleanengine/coin/order/application/port/out/WalletUpdatePort.java
index ff4bc069..a12fbc66 100644
--- a/src/main/java/com/cleanengine/coin/order/application/port/WalletUpdatePort.java
+++ b/src/main/java/com/cleanengine/coin/order/application/port/out/WalletUpdatePort.java
@@ -1,4 +1,4 @@
-package com.cleanengine.coin.order.application.port;
+package com.cleanengine.coin.order.application.port.out;
public interface WalletUpdatePort {
void lockAssetForSellOrder(Integer userId, String ticker, Double orderSize) throws RuntimeException;
diff --git a/src/main/java/com/cleanengine/coin/order/application/queue/OrderQueueManagerPool.java b/src/main/java/com/cleanengine/coin/order/application/queue/OrderQueueManagerPool.java
deleted file mode 100644
index d04c2734..00000000
--- a/src/main/java/com/cleanengine/coin/order/application/queue/OrderQueueManagerPool.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.cleanengine.coin.order.application.queue;
-
-import com.cleanengine.coin.order.domain.Order;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-
-import java.util.HashMap;
-import java.util.Optional;
-
-// TODO infra 계층으로 이동해야
-@Slf4j
-@Component
-public class OrderQueueManagerPool {
- // TODO 원래라면 OrderQueueUnit 각각이 Bean으로 등록되는게 깔끔할 수도 있다.
- private final HashMap orderQueueManagerMap = new HashMap<>();
-
- @Autowired
- public OrderQueueManagerPool(@Value("${order.tickers}") String[] tickers) {
- for(String ticker : tickers) {
- orderQueueManagerMap.put(ticker, new OrderQueueManager(ticker));
- }
- }
-
- // TODO 불필요 할지도
- public void addOrder(String ticker, Order order){
- Optional orderQueueManager = Optional.ofNullable(orderQueueManagerMap.get(ticker));
- orderQueueManager.ifPresent(queueManager -> queueManager.addOrder(order));
- }
-
- public OrderQueueManager getOrderQueueManager(String ticker){
- Optional orderQueueManager = Optional.ofNullable(orderQueueManagerMap.get(ticker));
- if(orderQueueManager.isEmpty()){
- log.debug("OrderQueueManager not found. check order.tickers on startup");
- throw new RuntimeException("OrderQueueManager not found. check order.tickers on startup");
- }
- return orderQueueManager.get();
- }
-}
diff --git a/src/main/java/com/cleanengine/coin/order/application/strategy/BuyOrderStrategy.java b/src/main/java/com/cleanengine/coin/order/application/strategy/BuyOrderStrategy.java
index 5a206b24..c03211af 100644
--- a/src/main/java/com/cleanengine/coin/order/application/strategy/BuyOrderStrategy.java
+++ b/src/main/java/com/cleanengine/coin/order/application/strategy/BuyOrderStrategy.java
@@ -1,37 +1,27 @@
package com.cleanengine.coin.order.application.strategy;
-import com.cleanengine.coin.order.application.OrderCommand;
-import com.cleanengine.coin.order.application.OrderInfo;
-import com.cleanengine.coin.order.application.port.AccountUpdatePort;
-import com.cleanengine.coin.order.application.queue.OrderQueueManagerPool;
+import com.cleanengine.coin.order.adapter.out.persistentce.account.AccountExternalRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.order.command.BuyOrderRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.wallet.WalletExternalRepository;
+import com.cleanengine.coin.order.application.AssetService;
+import com.cleanengine.coin.order.application.dto.OrderInfo;
+import com.cleanengine.coin.order.application.port.out.AccountUpdatePort;
+import com.cleanengine.coin.order.application.port.out.PublishOrderCreatedPort;
import com.cleanengine.coin.order.domain.BuyOrder;
import com.cleanengine.coin.order.domain.Order;
import com.cleanengine.coin.order.domain.domainservice.CreateBuyOrderDomainService;
-import com.cleanengine.coin.order.external.adapter.account.AccountExternalRepository;
-import com.cleanengine.coin.order.external.adapter.wallet.WalletExternalRepository;
-import com.cleanengine.coin.order.infra.BuyOrderRepository;
-import com.cleanengine.coin.orderbook.application.service.UpdateOrderBookUsecase;
-import com.cleanengine.coin.user.domain.Account;
-import com.cleanengine.coin.user.domain.Wallet;
-import lombok.RequiredArgsConstructor;
+import com.cleanengine.coin.order.domain.domainservice.CreateOrderDomainService;
import org.springframework.stereotype.Component;
@Component
-@RequiredArgsConstructor
public class BuyOrderStrategy extends CreateOrderStrategy> {
private final BuyOrderRepository buyOrderRepository;
- private final CreateBuyOrderDomainService createOrderDomainService;
- private final OrderQueueManagerPool orderQueueManagerPool;
private final AccountUpdatePort accountUpdatePort;
- private final UpdateOrderBookUsecase updateOrderBookUsecase;
- private final WalletExternalRepository walletRepository;
- private final AccountExternalRepository accountRepository;
+ private final CreateBuyOrderDomainService createOrderDomainService;
@Override
- public BuyOrder createOrder(OrderCommand.CreateOrder createOrderCommand) {
- return createOrderDomainService.createOrder(createOrderCommand.ticker(), createOrderCommand.userId(),
- createOrderCommand.isBuyOrder(), createOrderCommand.isMarketOrder(), createOrderCommand.orderSize(),
- createOrderCommand.price(), createOrderCommand.createdAt(), createOrderCommand.isBot());
+ public boolean supports(Boolean isBuyOrder) {
+ return isBuyOrder;
}
@Override
@@ -40,31 +30,30 @@ public void saveOrder(BuyOrder order) {
}
@Override
- protected void createWallet(Integer userId, String ticker) {
- if(walletRepository.findWalletBy(userId, ticker).isEmpty()){
- Account account = accountRepository.findByUserId(userId).orElseThrow();
- Wallet wallet = new Wallet(null, ticker, account.getId(), 0.0, 0.0, 0.0);
- walletRepository.save(wallet);
- }
- }
-
- @Override
- public OrderInfo.BuyOrderInfo extractOrderInfo(Order order) {
- return new OrderInfo.BuyOrderInfo((BuyOrder) order);
+ protected void keepHoldings(BuyOrder order) throws RuntimeException {
+ accountUpdatePort.lockDepositForBuyOrder(order.getUserId(), order.getLockedDeposit());
}
@Override
- public boolean supports(Boolean isBuyOrder) {
- return isBuyOrder;
+ protected CreateOrderDomainService createOrderDomainService() {
+ return createOrderDomainService;
}
@Override
- protected void keepHoldings(BuyOrder order) throws RuntimeException {
- accountUpdatePort.lockDepositForBuyOrder(order.getUserId(), order.getLockedDeposit());
+ protected OrderInfo.BuyOrderInfo extractOrderInfo(Order order) {
+ return new OrderInfo.BuyOrderInfo((BuyOrder) order);
}
- @Override
- protected void updateOrderBook(BuyOrder order) {
- updateOrderBookUsecase.updateOrderBookOnNewOrder(order);
+ public BuyOrderStrategy(PublishOrderCreatedPort publishOrderCreatedPort,
+ AssetService assetService,
+ WalletExternalRepository walletRepository,
+ AccountExternalRepository accountRepository,
+ BuyOrderRepository buyOrderRepository,
+ AccountUpdatePort accountUpdatePort,
+ CreateBuyOrderDomainService createOrderDomainService) {
+ super(publishOrderCreatedPort, assetService, walletRepository, accountRepository);
+ this.buyOrderRepository = buyOrderRepository;
+ this.accountUpdatePort = accountUpdatePort;
+ this.createOrderDomainService = createOrderDomainService;
}
}
diff --git a/src/main/java/com/cleanengine/coin/order/application/strategy/CreateOrderStrategy.java b/src/main/java/com/cleanengine/coin/order/application/strategy/CreateOrderStrategy.java
index 27c66efc..cfb42d28 100644
--- a/src/main/java/com/cleanengine/coin/order/application/strategy/CreateOrderStrategy.java
+++ b/src/main/java/com/cleanengine/coin/order/application/strategy/CreateOrderStrategy.java
@@ -1,27 +1,68 @@
package com.cleanengine.coin.order.application.strategy;
-import com.cleanengine.coin.order.application.OrderCommand;
-import com.cleanengine.coin.order.application.OrderInfo;
-import com.cleanengine.coin.order.application.queue.OrderQueueManagerPool;
+import com.cleanengine.coin.common.error.DomainValidationException;
+import com.cleanengine.coin.order.application.AssetService;
+import com.cleanengine.coin.order.application.dto.OrderCommand;
+import com.cleanengine.coin.order.application.dto.OrderInfo;
+import com.cleanengine.coin.order.application.event.OrderCreated;
+import com.cleanengine.coin.order.application.port.out.PublishOrderCreatedPort;
import com.cleanengine.coin.order.domain.Order;
+import com.cleanengine.coin.order.domain.domainservice.CreateOrderDomainService;
+import com.cleanengine.coin.order.adapter.out.persistentce.account.AccountExternalRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.wallet.WalletExternalRepository;
+import com.cleanengine.coin.user.domain.Account;
+import com.cleanengine.coin.user.domain.Wallet;
+import lombok.AllArgsConstructor;
+import org.springframework.validation.FieldError;
+import java.util.List;
+
+@AllArgsConstructor
public abstract class CreateOrderStrategy> {
+ protected final PublishOrderCreatedPort publishOrderCreatedPort;
+ protected final AssetService assetService;
+ protected final WalletExternalRepository walletRepository;
+ protected final AccountExternalRepository accountRepository;
- public T processCreatingOrder(OrderCommand.CreateOrder createOrderCommand){
+ public S processCreatingOrder(OrderCommand.CreateOrder createOrderCommand){
+ validateTicker(createOrderCommand.ticker());
T order = createOrder(createOrderCommand);
saveOrder(order);
- createWallet(order.getUserId(), order.getTicker());
+ createWalletIfNeeded(order.getUserId(), order.getTicker());
keepHoldings(order);
- updateOrderBook(order);
- return order;
+ publishOrderCreatedPort.publish(new OrderCreated(order));
+ return extractOrderInfo(order);
}
public abstract boolean supports(Boolean isBuyOrder);
- protected abstract T createOrder(OrderCommand.CreateOrder createOrderCommand);
protected abstract void saveOrder(T order);
- protected abstract void createWallet(Integer userId, String ticker);
protected abstract void keepHoldings(T order) throws RuntimeException;
- protected abstract void updateOrderBook(T order);
- public abstract S extractOrderInfo(Order order);
+ protected abstract CreateOrderDomainService createOrderDomainService();
+ protected abstract S extractOrderInfo(Order order);
+
+ protected void validateTicker(String ticker){
+ if(!assetService.isAssetExist(ticker)){
+ throw new DomainValidationException("Asset not supported "+ticker,
+ List.of(new FieldError("BuyOrder", "ticker", "Asset not supported")));
+ }
+ }
+
+ protected T createOrder(OrderCommand.CreateOrder createOrderCommand){
+ T order = createOrderDomainService().createOrder(
+ createOrderCommand.ticker(), createOrderCommand.userId(),
+ createOrderCommand.isBuyOrder(), createOrderCommand.isMarketOrder(),
+ createOrderCommand.orderSize(), createOrderCommand.price(),
+ createOrderCommand.createdAt(), createOrderCommand.isBot());
+
+ return order;
+ }
+
+ protected void createWalletIfNeeded(Integer userId, String ticker){
+ if(walletRepository.findWalletBy(userId, ticker).isEmpty()){
+ Account account = accountRepository.findByUserId(userId).orElseThrow();
+ Wallet wallet = Wallet.generateEmptyWallet(ticker, account.getId());
+ walletRepository.save(wallet);
+ }
+ }
}
diff --git a/src/main/java/com/cleanengine/coin/order/application/strategy/SellOrderStrategy.java b/src/main/java/com/cleanengine/coin/order/application/strategy/SellOrderStrategy.java
index cccf894a..cc64b6e5 100644
--- a/src/main/java/com/cleanengine/coin/order/application/strategy/SellOrderStrategy.java
+++ b/src/main/java/com/cleanengine/coin/order/application/strategy/SellOrderStrategy.java
@@ -1,35 +1,27 @@
package com.cleanengine.coin.order.application.strategy;
-import com.cleanengine.coin.order.application.OrderCommand;
-import com.cleanengine.coin.order.application.OrderInfo;
-import com.cleanengine.coin.order.application.port.WalletUpdatePort;
+import com.cleanengine.coin.order.adapter.out.persistentce.account.AccountExternalRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.order.command.SellOrderRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.wallet.WalletExternalRepository;
+import com.cleanengine.coin.order.application.AssetService;
+import com.cleanengine.coin.order.application.dto.OrderInfo;
+import com.cleanengine.coin.order.application.port.out.PublishOrderCreatedPort;
+import com.cleanengine.coin.order.application.port.out.WalletUpdatePort;
import com.cleanengine.coin.order.domain.Order;
import com.cleanengine.coin.order.domain.SellOrder;
+import com.cleanengine.coin.order.domain.domainservice.CreateOrderDomainService;
import com.cleanengine.coin.order.domain.domainservice.CreateSellOrderDomainService;
-import com.cleanengine.coin.order.external.adapter.account.AccountExternalRepository;
-import com.cleanengine.coin.order.external.adapter.wallet.WalletExternalRepository;
-import com.cleanengine.coin.order.infra.SellOrderRepository;
-import com.cleanengine.coin.orderbook.application.service.UpdateOrderBookUsecase;
-import com.cleanengine.coin.user.domain.Account;
-import com.cleanengine.coin.user.domain.Wallet;
-import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
-@RequiredArgsConstructor
public class SellOrderStrategy extends CreateOrderStrategy {
private final SellOrderRepository sellOrderRepository;
- private final CreateSellOrderDomainService createSellOrderDomainService;
private final WalletUpdatePort walletUpdatePort;
- private final UpdateOrderBookUsecase updateOrderBookUsecase;
- private final WalletExternalRepository walletRepository;
- private final AccountExternalRepository accountRepository;
+ private final CreateSellOrderDomainService createOrderDomainService;
@Override
- public SellOrder createOrder(OrderCommand.CreateOrder createOrderCommand) {
- return createSellOrderDomainService.createOrder(createOrderCommand.ticker(), createOrderCommand.userId(),
- createOrderCommand.isBuyOrder(), createOrderCommand.isMarketOrder(), createOrderCommand.orderSize(),
- createOrderCommand.price(), createOrderCommand.createdAt(), createOrderCommand.isBot());
+ public boolean supports(Boolean isBuyOrder) {
+ return !isBuyOrder;
}
@Override
@@ -38,31 +30,30 @@ public void saveOrder(SellOrder order) {
}
@Override
- protected void createWallet(Integer userId, String ticker) {
- if(walletRepository.findWalletBy(userId, ticker).isEmpty()){
- Account account = accountRepository.findByUserId(userId).orElseThrow();
- Wallet wallet = new Wallet(null, ticker, account.getId(), 0.0, 0.0, 0.0);
- walletRepository.save(wallet);
- }
- }
-
- @Override
- public OrderInfo.SellOrderInfo extractOrderInfo(Order order) {
- return new OrderInfo.SellOrderInfo((SellOrder) order);
+ protected void keepHoldings(SellOrder order) throws RuntimeException {
+ walletUpdatePort.lockAssetForSellOrder(order.getUserId(), order.getTicker(), order.getOrderSize());
}
@Override
- public boolean supports(Boolean isBuyOrder) {
- return !isBuyOrder;
+ protected CreateOrderDomainService createOrderDomainService() {
+ return createOrderDomainService;
}
@Override
- protected void keepHoldings(SellOrder order) throws RuntimeException {
- walletUpdatePort.lockAssetForSellOrder(order.getUserId(), order.getTicker(), order.getOrderSize());
+ protected OrderInfo.SellOrderInfo extractOrderInfo(Order order) {
+ return new OrderInfo.SellOrderInfo((SellOrder) order);
}
- @Override
- protected void updateOrderBook(SellOrder order) {
- updateOrderBookUsecase.updateOrderBookOnNewOrder(order);
+ public SellOrderStrategy(PublishOrderCreatedPort publishOrderCreatedPort,
+ AssetService assetService,
+ WalletExternalRepository walletRepository,
+ AccountExternalRepository accountRepository,
+ SellOrderRepository sellOrderRepository,
+ WalletUpdatePort walletUpdatePort,
+ CreateSellOrderDomainService createOrderDomainService) {
+ super(publishOrderCreatedPort, assetService, walletRepository, accountRepository);
+ this.sellOrderRepository = sellOrderRepository;
+ this.walletUpdatePort = walletUpdatePort;
+ this.createOrderDomainService = createOrderDomainService;
}
}
diff --git a/src/main/java/com/cleanengine/coin/order/domain/Asset.java b/src/main/java/com/cleanengine/coin/order/domain/Asset.java
index 448f10e5..5e70c9ac 100644
--- a/src/main/java/com/cleanengine/coin/order/domain/Asset.java
+++ b/src/main/java/com/cleanengine/coin/order/domain/Asset.java
@@ -1,13 +1,7 @@
package com.cleanengine.coin.order.domain;
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import jakarta.persistence.*;
+import lombok.*;
@Entity
@Table(name = "asset")
@@ -18,4 +12,11 @@ public class Asset {
private String ticker;
@Column(name = "name", length = 100)
private String name;
+ @Column(name = "icon", columnDefinition = "BLOB") @Lob @Setter
+ private byte[] icon;
+
+ public Asset(String ticker, String name){
+ this.ticker = ticker;
+ this.name = name;
+ }
}
diff --git a/src/main/java/com/cleanengine/coin/order/domain/Order.java b/src/main/java/com/cleanengine/coin/order/domain/Order.java
index d47e64ba..e5446a5f 100644
--- a/src/main/java/com/cleanengine/coin/order/domain/Order.java
+++ b/src/main/java/com/cleanengine/coin/order/domain/Order.java
@@ -5,6 +5,7 @@
import lombok.Setter;
import java.time.LocalDateTime;
+import java.util.Objects;
@MappedSuperclass
@Getter
@@ -42,4 +43,17 @@ public abstract class Order {
@Column(name="is_bot", nullable = false, updatable = false)
protected Boolean isBot;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || this.getClass() != o.getClass()) return false;
+ Order order = (Order) o;
+ return id.equals(order.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.getClass(), this.id);
+ }
}
diff --git a/src/main/java/com/cleanengine/coin/order/domain/OrderType.java b/src/main/java/com/cleanengine/coin/order/domain/OrderType.java
new file mode 100644
index 00000000..d0831546
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/domain/OrderType.java
@@ -0,0 +1,5 @@
+package com.cleanengine.coin.order.domain;
+
+public enum OrderType {
+ MARKET, LIMIT
+}
diff --git a/src/main/java/com/cleanengine/coin/order/domain/spi/ActiveOrders.java b/src/main/java/com/cleanengine/coin/order/domain/spi/ActiveOrders.java
new file mode 100644
index 00000000..407fdf6e
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/domain/spi/ActiveOrders.java
@@ -0,0 +1,21 @@
+package com.cleanengine.coin.order.domain.spi;
+
+import com.cleanengine.coin.common.domain.port.KeyValueStore;
+import com.cleanengine.coin.order.domain.BuyOrder;
+import com.cleanengine.coin.order.domain.Order;
+import com.cleanengine.coin.order.domain.SellOrder;
+
+import java.util.Optional;
+
+public interface ActiveOrders {
+ String getTicker();
+
+ void saveOrder(Order order);
+
+ Optional getOrder(Long orderId, boolean isBuyOrder);
+
+ Optional removeOrder(Long orderId, boolean isBuyOrder);
+
+ KeyValueStore getBuyOrderKeyValueStore();
+ KeyValueStore getSellOrderKeyValueStore();
+}
diff --git a/src/main/java/com/cleanengine/coin/order/domain/spi/ActiveOrdersManager.java b/src/main/java/com/cleanengine/coin/order/domain/spi/ActiveOrdersManager.java
new file mode 100644
index 00000000..d8f612bf
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/domain/spi/ActiveOrdersManager.java
@@ -0,0 +1,13 @@
+package com.cleanengine.coin.order.domain.spi;
+
+import java.io.Closeable;
+
+public interface ActiveOrdersManager extends Closeable {
+
+ ActiveOrders getActiveOrders(String ticker);
+
+ void removeActiveOrders(String ticker);
+
+ @Override
+ void close();
+}
diff --git a/src/main/java/com/cleanengine/coin/order/domain/spi/WaitingOrders.java b/src/main/java/com/cleanengine/coin/order/domain/spi/WaitingOrders.java
new file mode 100644
index 00000000..1add4745
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/domain/spi/WaitingOrders.java
@@ -0,0 +1,27 @@
+package com.cleanengine.coin.order.domain.spi;
+
+import com.cleanengine.coin.common.domain.port.PriorityQueueStore;
+import com.cleanengine.coin.order.domain.BuyOrder;
+import com.cleanengine.coin.order.domain.Order;
+import com.cleanengine.coin.order.domain.OrderType;
+import com.cleanengine.coin.order.domain.SellOrder;
+
+import java.io.Closeable;
+
+public interface WaitingOrders extends Closeable {
+
+ String getTicker();
+
+ void addOrder(Order order);
+
+ PriorityQueueStore getBuyOrderPriorityQueueStore(OrderType orderType);
+ PriorityQueueStore getSellOrderPriorityQueueStore(OrderType orderType);
+
+ // TODO removeOrder 여기에 있는 것이 나을지 고민
+ void removeOrder(Order order);
+
+ void clearAllQueues();
+
+ @Override
+ void close();
+}
diff --git a/src/main/java/com/cleanengine/coin/order/domain/spi/WaitingOrdersManager.java b/src/main/java/com/cleanengine/coin/order/domain/spi/WaitingOrdersManager.java
new file mode 100644
index 00000000..48a04171
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/order/domain/spi/WaitingOrdersManager.java
@@ -0,0 +1,12 @@
+package com.cleanengine.coin.order.domain.spi;
+
+import java.io.Closeable;
+
+public interface WaitingOrdersManager extends Closeable {
+ WaitingOrders getWaitingOrders(String ticker);
+
+ void removeWaitingOrders(String ticker);
+
+ @Override
+ void close();
+}
diff --git a/src/main/java/com/cleanengine/coin/order/infra/ActiveOrderManager.java b/src/main/java/com/cleanengine/coin/order/infra/ActiveOrderManager.java
deleted file mode 100644
index 2d855962..00000000
--- a/src/main/java/com/cleanengine/coin/order/infra/ActiveOrderManager.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.cleanengine.coin.order.infra;
-
-import com.cleanengine.coin.order.domain.BuyOrder;
-import com.cleanengine.coin.order.domain.Order;
-import com.cleanengine.coin.order.domain.SellOrder;
-import lombok.Getter;
-
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
-
-public class ActiveOrderManager {
- @Getter
- private final String ticker;
- private final ConcurrentHashMap activeBuyOrders = new ConcurrentHashMap<>();
- private final ConcurrentHashMap activeSellOrders = new ConcurrentHashMap<>();
-
- public ActiveOrderManager(String ticker) {
- this.ticker = ticker;
- }
-
- public void saveOrder(Order order) {
- if(order.getIsMarketOrder()){
- return;
- }
- if(order instanceof BuyOrder){
- activeBuyOrders.put(order.getId(), (BuyOrder) order);
- } else {
- activeSellOrders.put(order.getId(), (SellOrder) order);
- }
- }
-
- public Optional getOrder(Long orderId, boolean isBuyOrder) {
- if(isBuyOrder) {
- return Optional.ofNullable(activeBuyOrders.get(orderId));
- } else {
- return Optional.ofNullable(activeSellOrders.get(orderId));
- }
- }
-
- public void removeOrder(Long orderId, boolean isBuyOrder) {
- if(isBuyOrder) {
- activeBuyOrders.remove(orderId);
- } else {
- activeSellOrders.remove(orderId);
- }
- }
-}
diff --git a/src/main/java/com/cleanengine/coin/order/infra/ActiveOrderManagerPool.java b/src/main/java/com/cleanengine/coin/order/infra/ActiveOrderManagerPool.java
deleted file mode 100644
index 9bced8b8..00000000
--- a/src/main/java/com/cleanengine/coin/order/infra/ActiveOrderManagerPool.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.cleanengine.coin.order.infra;
-
-import com.cleanengine.coin.order.domain.Order;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-
-import java.util.HashMap;
-import java.util.Optional;
-
-@Slf4j
-@Component
-public class ActiveOrderManagerPool {
- private final HashMap activeOrderManagerPool = new HashMap<>();
-
- @Autowired
- public ActiveOrderManagerPool(@Value("${order.tickers}") String[] tickers) {
- for (String ticker : tickers) {
- activeOrderManagerPool.put(ticker, new ActiveOrderManager(ticker));
- }
- }
-
- public void saveOrder(String ticker, Order order) {
- ActiveOrderManager activeOrderManager = getActiveOrderManager(ticker);
- activeOrderManager.saveOrder(order);
- }
-
- public Optional getOrder(String ticker, Long orderId, boolean isBuyOrder) {
- ActiveOrderManager activeOrderManager = getActiveOrderManager(ticker);
- return activeOrderManager.getOrder(orderId,isBuyOrder);
- }
-
- public void removeOrder(String ticker, Long orderId, boolean isBuyOrder) {
- ActiveOrderManager activeOrderManager = getActiveOrderManager(ticker);
- activeOrderManager.removeOrder(orderId, isBuyOrder);
- }
-
- private ActiveOrderManager getActiveOrderManager(String ticker) {
- Optional activeOrderManager = Optional.ofNullable(activeOrderManagerPool.get(ticker));
- if(activeOrderManager.isEmpty()){
- log.debug("ActiveOrderManager not found. check order.tickers on startup");
- throw new RuntimeException("ActiveOrderManager not found. check order.tickers on startup");
- }
- return activeOrderManager.get();
- }
-}
diff --git a/src/main/java/com/cleanengine/coin/order/infra/OrderCreatedEventHandler.java b/src/main/java/com/cleanengine/coin/order/infra/OrderCreatedEventHandler.java
deleted file mode 100644
index 2b561df0..00000000
--- a/src/main/java/com/cleanengine/coin/order/infra/OrderCreatedEventHandler.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.cleanengine.coin.order.infra;
-
-import com.cleanengine.coin.order.application.OrderCreated;
-import com.cleanengine.coin.order.application.queue.OrderQueueManagerPool;
-import com.cleanengine.coin.order.domain.Order;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.event.TransactionalEventListener;
-
-@Component
-@RequiredArgsConstructor
-public class OrderCreatedEventHandler {
- private final OrderQueueManagerPool orderQueueManagerPool;
-
- @TransactionalEventListener(OrderCreated.class)
- public void handleOrderCreated(OrderCreated event) {
- Order order = event.getOrder();
- orderQueueManagerPool.addOrder(order.getTicker(), order);
- }
-}
diff --git a/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java b/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java
index ede41fc9..8d5358d3 100644
--- a/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java
+++ b/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java
@@ -3,7 +3,8 @@
import com.cleanengine.coin.order.domain.BuyOrder;
import com.cleanengine.coin.order.domain.Order;
import com.cleanengine.coin.order.domain.OrderStatus;
-import com.cleanengine.coin.order.infra.ActiveOrderManagerPool;
+import com.cleanengine.coin.order.domain.spi.ActiveOrders;
+import com.cleanengine.coin.order.domain.spi.ActiveOrdersManager;
import com.cleanengine.coin.orderbook.domain.OrderBookDomainService;
import com.cleanengine.coin.orderbook.dto.OrderBookInfo;
import com.cleanengine.coin.orderbook.dto.OrderBookUnitInfo;
@@ -18,7 +19,7 @@
@Component
@RequiredArgsConstructor
public class OrderBookService implements UpdateOrderBookUsecase, ReadOrderBookUsecase {
- private final ActiveOrderManagerPool activeOrderManagerPool;
+ private final ActiveOrdersManager activeOrdersManager;
private final OrderBookDomainService orderBookDomainService;
private final OrderBookUpdatedNotifierPort orderBookUpdatedNotifierPort;
@@ -26,7 +27,7 @@ public class OrderBookService implements UpdateOrderBookUsecase, ReadOrderBookUs
public void updateOrderBookOnNewOrder(Order order) {
if(order.getIsMarketOrder()){return;}
String ticker = order.getTicker();
- activeOrderManagerPool.saveOrder(ticker, order);
+ activeOrders(ticker).saveOrder(order);
boolean isBuyOrder = order instanceof BuyOrder;
orderBookDomainService.updateOrderBookOnNewOrder(ticker, isBuyOrder, order.getPrice(), order.getOrderSize());
@@ -43,13 +44,15 @@ public void updateOrderBookOnTradeExecuted(String ticker, Long buyOrderId, Long
}
private void updateOrderBookOnTradeExecuted(String ticker, Long orderId, boolean isBuyOrder, Double orderSize) {
- Optional orderOptional = activeOrderManagerPool.getOrder(ticker, orderId, isBuyOrder);
+ ActiveOrders activeOrders = activeOrders(ticker);
+
+ Optional orderOptional = activeOrders.getOrder(orderId, isBuyOrder);
// 시장가일 경우에는 ManagerPool에 없음
if(orderOptional.isPresent()){
Order order = orderOptional.get();
orderBookDomainService.updateOrderBookOnTradeExecuted(ticker, isBuyOrder, order.getPrice(), orderSize);
if(order.getState().equals(OrderStatus.DONE) || approxEquals(order.getOrderSize(), 0.0)){
- activeOrderManagerPool.removeOrder(ticker, orderId, isBuyOrder);
+ activeOrders.removeOrder(orderId, isBuyOrder);
}
}
}
@@ -72,4 +75,8 @@ private void sendOrderBookUpdated(String ticker){
public OrderBookInfo getOrderBook(String ticker) {
return extractOrderBookInfo(ticker);
}
+
+ private ActiveOrders activeOrders(String ticker){
+ return activeOrdersManager.getActiveOrders(ticker);
+ }
}
diff --git a/src/main/java/com/cleanengine/coin/orderbook/domain/OrderBookDomainService.java b/src/main/java/com/cleanengine/coin/orderbook/domain/OrderBookDomainService.java
index 0efbd6c2..c1f644fb 100644
--- a/src/main/java/com/cleanengine/coin/orderbook/domain/OrderBookDomainService.java
+++ b/src/main/java/com/cleanengine/coin/orderbook/domain/OrderBookDomainService.java
@@ -1,11 +1,7 @@
package com.cleanengine.coin.orderbook.domain;
-import com.cleanengine.coin.common.error.DomainValidationException;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
-import org.springframework.validation.FieldError;
import java.util.HashMap;
import java.util.List;
@@ -16,12 +12,6 @@
@Component
public class OrderBookDomainService {
private final HashMap orderBookPool = new HashMap<>();
- @Autowired
- public OrderBookDomainService(@Value("${order.tickers}") String[] tickers) {
- for (String ticker : tickers) {
- orderBookPool.put(ticker, new OrderBook(ticker));
- }
- }
public void updateOrderBookOnNewOrder(String ticker, boolean isBuyOrder, Double price, Double orderSize) {
getOrderBook(ticker).updateOrderBookOnNewOrder(isBuyOrder, price, orderSize);
@@ -39,14 +29,23 @@ public List getSellOrderBookList(String ticker, int size) {
return getOrderBook(ticker).getSellOrderBookList(size);
}
- private OrderBook getOrderBook(String ticker) {
- Optional orderBook = Optional.ofNullable(orderBookPool.get(ticker));
- if(orderBook.isEmpty()){
- String message = "OrderBook not found. Check ticker sent from client or order.tickers on startup";
- log.debug(message);
- throw new DomainValidationException(message,
- List.of(new FieldError("OrderBook", "ticker", message)));
+ protected OrderBook getOrderBook(String ticker) {
+ if(!orderBookPool.containsKey(ticker)){
+ addOrderBook(ticker);
+ }
+
+ Optional orderBookOpt = Optional.ofNullable(orderBookPool.get(ticker));
+ if(orderBookOpt.isEmpty()){
+ log.debug("OrderBook not found. with " + ticker);
+ throw new RuntimeException("OrderBook not found with " + ticker);
+ }
+
+ return orderBookOpt.get();
+ }
+
+ protected synchronized void addOrderBook(String ticker){
+ if(!orderBookPool.containsKey(ticker)){
+ orderBookPool.put(ticker, new OrderBook(ticker));
}
- return orderBook.get();
}
}
diff --git a/src/main/java/com/cleanengine/coin/orderbook/infra/SpringTradeExecutedUpdateOrderBookHandler.java b/src/main/java/com/cleanengine/coin/orderbook/infra/SpringTradeExecutedUpdateOrderBookHandler.java
new file mode 100644
index 00000000..6d96f693
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/orderbook/infra/SpringTradeExecutedUpdateOrderBookHandler.java
@@ -0,0 +1,21 @@
+package com.cleanengine.coin.orderbook.infra;
+
+import com.cleanengine.coin.orderbook.application.service.UpdateOrderBookUsecase;
+import com.cleanengine.coin.trade.application.TradeExecutedEvent;
+import com.cleanengine.coin.trade.entity.Trade;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+@Component
+@RequiredArgsConstructor
+public class SpringTradeExecutedUpdateOrderBookHandler {
+ private final UpdateOrderBookUsecase updateOrderBookUsecase;
+
+ @TransactionalEventListener
+ public void onTradeExecutedEvent(TradeExecutedEvent tradeExecutedEvent) {
+ Trade trade = tradeExecutedEvent.getTrade();
+ updateOrderBookUsecase.updateOrderBookOnTradeExecuted(trade.getTicker(),
+ tradeExecutedEvent.getBuyOrderId(), tradeExecutedEvent.getSellOrderId(), trade.getSize());
+ }
+}
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 64c48c5f..1753e944 100644
--- a/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java
+++ b/src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java
@@ -1,83 +1,78 @@
package com.cleanengine.coin.realitybot.api;
import com.cleanengine.coin.common.annotation.WorkingServerProfile;
+import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository;
+import com.cleanengine.coin.order.domain.Asset;
import com.cleanengine.coin.realitybot.dto.Ticks;
+import com.cleanengine.coin.realitybot.service.ApiVWAPService;
import com.cleanengine.coin.realitybot.service.OrderGenerateService;
-import com.cleanengine.coin.realitybot.service.TickService;
-import com.cleanengine.coin.realitybot.service.VirtualTradeService;
+import com.cleanengine.coin.realitybot.service.TickParser;
+import com.cleanengine.coin.realitybot.service.TickServiceManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.DisposableBean;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
-import java.util.Queue;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@WorkingServerProfile
@RequiredArgsConstructor
-public class ApiScheduler implements DisposableBean {
+public class ApiScheduler {
private final BithumbAPIClient bithumbAPIClient;
- private final TickService tickService;
+ private final TickParser tickParser;
private final OrderGenerateService orderGenerateService;
- private long lastMaxSequentialId = 1L;
- private final Queue ticksQueue;
- private final VirtualTradeService virtualTradeService;
+ private final TickServiceManager tickServiceManager;
+ private final Map lastSequentialIdMap = new ConcurrentHashMap<>();
+ private final AssetRepository assetRepository;
+ private String ticker;
+
+ @Scheduled(fixedRate = 5000)
+ public void MarketAllRequest() throws InterruptedException {
+ List tickers = assetRepository.findAll();
+ for (Asset ticker : tickers){
+ String tickerName = ticker.getTicker();
+ MarketDataRequest(tickerName);
+ Thread.sleep(500);
+ }
+ }
+ public void MarketDataRequest(String ticker){
+ this.ticker = ticker;
+ String rawJson = bithumbAPIClient.get(ticker); //api raw데이터
+ List gson = TickParser.parseGson(rawJson); //json을 list로 변환
- @Scheduled(fixedRate = 5000) //5초마다 실행
- public void MarketDataRequest(){
- String rawJson = bithumbAPIClient.get(); //api raw데이터
- List gson = TickService.paraseGson(rawJson); //json을 list로 변환
+ ApiVWAPService apiVWAPService = 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() > lastMaxSequentialId){ //중복 검증용
-
- //추세 갯수 지정 및 queue관리
- if (ticksQueue.size()>=10){ //갯수를 10개 까지만으로 제한
- ticksQueue.poll(); //queue에서 빼기, 추세를 구현하기만 하면 필요가 있을까? -> 없음
- }//sequentialId가 역으로 받아 poll을 먼저해야 add가능
+ if (ticks.getSequential_id() > lastSeqId){ //중복 검증용
+ apiVWAPService.addTick(ticks);
+ lastSeqId = Math.max(lastSeqId, ticks.getSequential_id()); //중복 id 갱신
- //중복 검사 후 ticks 추가
- ticksQueue.add(ticks);
- lastMaxSequentialId = Math.max(lastMaxSequentialId, ticks.getSequential_id()); //중복 id 갱신
-
- /*모니터링용
- System.out.println(ticks.getSequential_id());
- System.out.println("if = "+ticksQueue.size());
- */
}
}
-
- //api 값으로 추세(VWAP)와 가상추세(VirtualVWAP) 구하기
- if (ticksQueue.size()>=10){ //10개 이전 작동시 order 에런 발생 (-300~300원 대량주문 / 호가 단위 때문에)
-
- tickService.processVWAP();//평균 체결 금액(VWAP) 구하기 (추세)
-
- /*//모니터링용
- log.info("generateOrder vwap 확인용 = {}",tickService.getVwap());
- log.info("generateOrder volume 확인용 = {}",tickService.getTotalVolume());*/
-
- //생성 된 vwap으로 주문 로직 실행 TODO 비동기로 전환하기
- orderGenerateService.generateOrder(tickService.getVwap(),(tickService.getTotalVolume()/30)); //1tick 당 매수/매도 3개씩 제작
-// virtualTradeService.matchOrder();//일치하면 체결 진행 TODO 합칠 때 제거
- virtualTradeService.matchOrderbyIterator();//일치하면 체결 진행 TODO 합칠 때 제거
- }
-
+ lastSequentialIdMap.put(ticker,lastSeqId);
+ double vwap = apiVWAPService.getVWAP();
+ double volume = apiVWAPService.getAvgVolumePerOrder();
+ orderGenerateService.generateOrder(ticker,vwap,volume); //1tick 당 매수/매도 3개씩 제작
+// log.info("작동확인 {}의 가격 : {} , 볼륨 : {}",ticker, vwap, volume);
}
- @Override
+
+/* @Override
public void destroy() throws Exception { //담긴 Queue데이터 확인용
// log.info("종료 전 큐 데이터 출력");
- ticksQueue.forEach(tick -> log.debug(tick.toString())); //
+// ticksQueue.forEach(tick -> log.debug(tick.toString())); //
// log.info("총 {}건의 데이터 출력 완료",ticksQueue.size());
// orderQueueManagerService.logAllOrders();
// virtualTradeService.printOrderSummary();
- }
+ }*/
}
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 6d98d129..a0ee5f31 100644
--- a/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java
+++ b/src/main/java/com/cleanengine/coin/realitybot/api/BithumbAPIClient.java
@@ -16,19 +16,22 @@
public class BithumbAPIClient {
private OkHttpClient client;
private Gson gson;
+ private String ticker;
- public String get(){ //API를 responseBody에 담아 반환
+
+ public String get(String ticker){ //API를 responseBody에 담아 반환
+ this.ticker = ticker;
client = new OkHttpClient();
gson = new Gson();
Request request = new Request.Builder()
- .url("https://api.bithumb.com/v1/trades/ticks?market=krw-TRUMP&count=10")
+ .url("https://api.bithumb.com/v1/trades/ticks?market=krw-"+ticker+"&count=10")
.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 응답 : {}",responseBody);
+ log.debug("{}의 Bithumb API 응답 : {}",ticker,responseBody);
return responseBody;
} catch (IOException e) {
throw new RuntimeException(e);
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 9d234809..6de9a720 100644
--- a/src/main/java/com/cleanengine/coin/realitybot/controller/ApiController.java
+++ b/src/main/java/com/cleanengine/coin/realitybot/controller/ApiController.java
@@ -3,7 +3,6 @@
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@@ -15,6 +14,7 @@ public ApiController(BithumbAPIClient bithumbAPIClient) {
}
@GetMapping("/test")
public String getApiData(){
- return bithumbAPIClient.get();
+// return bithumbAPIClient.get(ticekr);
+ return null;
}}
diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/ApiVWAPService.java b/src/main/java/com/cleanengine/coin/realitybot/service/ApiVWAPService.java
new file mode 100644
index 00000000..e1a9b0b3
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/realitybot/service/ApiVWAPService.java
@@ -0,0 +1,47 @@
+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/OrderGenerateService.java b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java
index 88d1a16f..3ba308b2 100644
--- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java
+++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderGenerateService.java
@@ -1,18 +1,19 @@
package com.cleanengine.coin.realitybot.service;
import com.cleanengine.coin.common.error.DomainValidationException;
-import com.cleanengine.coin.configuration.bootstrap.DBInitRunner;
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.order.adapter.out.persistentce.account.AccountExternalRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.wallet.WalletExternalRepository;
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.core.annotation.Order;
import org.springframework.stereotype.Service;
+import java.text.DecimalFormat;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -21,49 +22,39 @@
@Slf4j
@Service
+@Order(5)
@RequiredArgsConstructor
public class OrderGenerateService {
private final int[] orderLevels = {1,2,3};
private final int unitPrice = 10; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요
- private final OrderQueueManagerService queueManager;
private final PlatformVWAPService platformVWAPService;
private final OrderService orderService;
private final TradeRepository tradeRepository;
private final VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler;
private final WalletExternalRepository walletExternalRepository;
private final AccountExternalRepository accountExternalRepository;
+ private String ticker;
- //TODO 비동기 처리로 전환 필요 + 시장 확인 후 주문량에 따른 오더 조절 필요
- public void generateOrder(double apiVWAP, double avgVolum) {//기준 주문금액, 주문량 받기 (tick당 계산되어 들어옴)
+ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//기준 주문금액, 주문량 받기 (tick당 계산되어 들어옴)
+ this.ticker = ticker;
- //todo 여기에 recordTrade를 불러 올 예정
- //불러와서 vwap을 넣어야 함
- List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc("TRUMP");
+ //최근 체결 내역 가져오기
+ List trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker);
- // Platform 기반 가격 , 최초 0.0원
-// double platformVWAP = platformVWAPService.getPlatformVWAP(); //order를 넣고 체결한 trade queue 값을 기준으로 계산
- double platformVWAP = platformVWAPService.calculateVWAPbyTrades(trades);
- //todo 0513 이거 체결량 그만큼 채워지는 거 아니면 작동 안되도록 전환 필요
- if(platformVWAP == 0.0){ //최초 실행 시 vwap 계산
- platformVWAP = generateVirtualVWAP(apiVWAP); //0.1% 보정값 랜덤 생성
- }
+ // Platform 기반 가격 생성 (10개 이하, 10개 이상에 따른 가격 생성)
+ double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,trades,apiVWAP);
- //근데 이거 platforVWAP 이 값이 있을 경우 작동해야 함
+ //편차 계산 (vwap 기준)
double trendLineRate = (platformVWAP - apiVWAP)/ apiVWAP;
- boolean isWithinRange = Math.abs(trendLineRate) <= 0.01;
-
- //VWAP 선택기 = 최초1회 API, 이후 Tick기반 todo 제거 대상
-// double virtualVWAP = virtualMarketService.switcherVWAP(tickService.getTicksQueue(), platformVWAP);
-
-
- //TODO 체결 제작 후 VirtualVWAP이 아닌 PlatformVWAP 로 전환필요 ✔
+ //편차가 +-1% 이상 발생하면 true 반환
+ boolean isWithinRange = Math.abs(trendLineRate) <= 0.001; //TODO 호가 단위에 따른 편차 보정 필요
for(int level : orderLevels) { //1주문당 3회 매수매도 처리
- double priceOffset = unitPrice * level; //3단계 호가 각각 처리
- double randomOffset = level1TradeMaker(platformVWAP,getDynamicMaxRate(trendLineRate));//랜덤 오차 부여 (가격 조정용)
- double deviation = Math.abs(trendLineRate);
-
+ 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;
@@ -86,9 +77,8 @@ public void generateOrder(double apiVWAP, double avgVolum) {//기준 주문금
}
//주문 실행
-// sellPrice = normalizeToUnit(virtualVWAP + priceOffset);//todo : 제거 대상
- double sellVolume = getRandomVolum(avgVolum);
- double buyVolume = getRandomVolum(avgVolum);
+ double sellVolume = getRandomVolum(avgVolume);
+ double buyVolume = getRandomVolum(avgVolume);
if (platformVWAP != 0){
if (isWithinRange){
@@ -102,11 +92,11 @@ public void generateOrder(double apiVWAP, double avgVolum) {//기준 주문금
}
double correctionRate = 0.1;
if (trendLineRate < -0.01) { // platformVWAP이 너무 낮음
- sellPrice = normalizeToUnit(sellPrice - (apiVWAP * correctionRate)); // 매도 더 싸게 → 체결 유도
- buyPrice = normalizeToUnit(buyPrice + (apiVWAP * correctionRate)); // 매수 더 비싸게 → 체결 유도
+ 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)); // 매수 더 싸게
+ sellPrice = normalizeToUnit(sellPrice - (apiVWAP * correctionRate)); // 매도 싸게
+ buyPrice = normalizeToUnit(buyPrice - (apiVWAP * correctionRate)); // 매수 싸게
//platform vwap -> vwap으로 변환
}
@@ -115,26 +105,30 @@ public void generateOrder(double apiVWAP, double avgVolum) {//기준 주문금
if (deviation > 0.01) {
double power = trendLineRate * 100; // 3% → 3
if (trendLineRate < 0) {
- buyVolume *= 1.0 + power * 0.5; // 3% → 2.5배
+ 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 {
- sellVolume *= 1.0 + power * 0.5;
+ 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("TRUMP",false, sellVolume,sellPrice);
- createOrderWithFallback("TRUMP",true, buyVolume,buyPrice);
+ createOrderWithFallback(ticker,false, sellVolume,sellPrice);
+ createOrderWithFallback(ticker,true, buyVolume,buyPrice);
- queueManager.addSellOrder(sellPrice, sellVolume);
- queueManager.addBuyOrder(buyPrice, buyVolume); //Queue 추가
+// queueManager.addSellOrder(sellPrice, sellVolume);
+// queueManager.addBuyOrder(buyPrice, buyVolume); //Queue 추가
} else {
//스위치 시켜야 할까?
- createOrderWithFallback("TRUMP",false, sellVolume,sellPrice);
- createOrderWithFallback("TRUMP",true, buyVolume,buyPrice);
+ createOrderWithFallback(ticker,false, sellVolume,sellPrice);
+ createOrderWithFallback(ticker,true, buyVolume,buyPrice);
- queueManager.addSellOrder(sellPrice, sellVolume);
- queueManager.addBuyOrder(buyPrice, buyVolume);
+// queueManager.addSellOrder(sellPrice, sellVolume);
+// queueManager.addBuyOrder(buyPrice, buyVolume);
}
@@ -145,46 +139,41 @@ public void generateOrder(double apiVWAP, double avgVolum) {//기준 주문금
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
+// vwaPerrorInJectionScheduler.enableInjection(); //에러 발생기 비활성화
-/* //모니터링용
+ /* //모니터링용
System.out.println("sellPrice = " + sellPrice);
System.out.println("sellVolume = " + sellVolume);
-// buyPrice = normalizeToUnit(virtualVWAP - priceOffset);//todo : 제거 대상
//모니터링용
System.out.println("buyPrice = " + buyPrice);
System.out.println("buyVolume = " + buyVolume);
System.out.println("====================================");
- System.out.println("현재 시장 vwap "+apiVWAP+" 현재 플랫폼 vwap"+platformVWAP);
- System.out.println("====================================");*/
-// vwaPerrorInJectionScheduler.enableInjection(); //에러 발생기 비활성화
+ DecimalFormat df = new DecimalFormat("#,##0.00");
+ System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP));*/
+
}
-/* System.out.println("📦 [체결 기록 Top 10]");
+ /* System.out.println("📦"+ticker+" [체결 기록 Top 10]");
trades.forEach(t ->
- System.out.printf("🕒 %s | 가격: %.0f | 수량: %.2f | 매수: #%d ↔ 매도: #%d%n",
+ System.out.printf("🕒 %s | 가격: %.0f | 수량: %.8f | 매수: #%d ↔ 매도: #%d%n",
t.getTradeTime(), t.getPrice(), t.getSize(), t.getBuyUserId(), t.getSellUserId())
);*/
}
- //todo VirtualMarketService 여기에도 있는데 공통화 필요? , 계수는 조금 다른긴함 -> vms 제거 대상
- private double generateVirtualVWAP(double apiVWAP) {//가상 vwap 계산 , 최초 1회 사용
- double maxDeviationaRate = 0.001; //보정값 0.1%만
- double deviation = (Math.random() * 2 - 1)* maxDeviationaRate; //편차 계산
- return apiVWAP * (1+deviation); // +=deviation 난수 생성 후 계산 (범위는 -1~+1)
- }
-
-
- private double getDynamicMaxRate(double trendLineRate) {
- return 0.01 + Math.abs(trendLineRate) * 0.5; // 더 벌어질수록 보정폭 확대
- }
-
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;
+ }
+
try {
orderService.createOrderWithBot(ticker, isBuy, volume, price);
} catch (DomainValidationException e) {
log.debug("잔량 부족: {}", e.getMessage());
try {
- resetBot();
+ resetBot(ticker);
orderService.createOrderWithBot(ticker, isBuy, volume, price);
} catch (Exception e1) {
log.error("주문 재시도 실패", e1);
@@ -192,10 +181,11 @@ private void createOrderWithFallback(String ticker,boolean isBuy, double volume,
}
}
- protected void resetBot(){
- Wallet wallet = walletExternalRepository.findWalletBy(SELL_ORDER_BOT_ID,"TRUMP").get();
+ protected void resetBot(String ticker){
+ this.ticker = ticker;
+ Wallet wallet = walletExternalRepository.findWalletBy(SELL_ORDER_BOT_ID,ticker).get();
wallet.setSize(500_000_000.0);
- Wallet wallet2 = walletExternalRepository.findWalletBy(BUY_ORDER_BOT_ID,"TRUMP").get();
+ Wallet wallet2 = walletExternalRepository.findWalletBy(BUY_ORDER_BOT_ID,ticker).get();
wallet2.setSize(0.0);
walletExternalRepository.save(wallet);
walletExternalRepository.save(wallet2);
@@ -210,17 +200,30 @@ protected void resetBot(){
//==================================order 정규화용 ============================================
- private double level1TradeMaker(double apiVWAP, double maxRate){
+ private double level1TradeMaker(double platformVWAP, double maxRate){
+ //시장가에 해당하는 호가는 거래 체결 강하게 하기 위함
double percent = (Math.random() * 2-1)*maxRate;
- return apiVWAP * percent;
+ 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)(Math.round(price / unitPrice)) * unitPrice;
-// return (int) (price / unitPrice) * unitPrice;
}
private double getRandomVolum(double avgVolum){ //볼륨 랜덤 입력
double rawVolume = avgVolum * (0.5+Math.random());
- return Math.round(rawVolume * 1000.0)/1000.0;
+ //호가 단위에 따라 0원이 발생 가능성
+ double resultVolume = Math.round(rawVolume * 1000.0)/1000.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/OrderQueueManagerService.java b/src/main/java/com/cleanengine/coin/realitybot/service/OrderQueueManagerService.java
index e2917f06..f737db01 100644
--- a/src/main/java/com/cleanengine/coin/realitybot/service/OrderQueueManagerService.java
+++ b/src/main/java/com/cleanengine/coin/realitybot/service/OrderQueueManagerService.java
@@ -2,14 +2,16 @@
import com.cleanengine.coin.realitybot.dto.TestOrder;
import lombok.Getter;
-import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.PriorityQueue;
@Getter
-@Component
+//@Component
public class OrderQueueManagerService {
+ /*해당 코드는 초기 개별 모듈로 작업할 때 가상의 체결을 만드는 코드였습니다.
+ * 이젠 쓰이지 않는 코드이나 어떤 에러가 발생할 때 재사용하기 위한 용도로 삭제하지 않았습니다.
+ * */
//체결용 출력
private final PriorityQueue buyqueue = new PriorityQueue<>(new Comparator() {
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 f89e5a8b..d3b0821f 100644
--- a/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java
+++ b/src/main/java/com/cleanengine/coin/realitybot/service/PlatformVWAPService.java
@@ -1,69 +1,32 @@
package com.cleanengine.coin.realitybot.service;
+import com.cleanengine.coin.realitybot.vo.VWAPState;
import com.cleanengine.coin.trade.entity.Trade;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
-import java.util.LinkedList;
import java.util.List;
-import java.util.Queue;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
@Service
+@Slf4j
public class PlatformVWAPService {//TODO 가상 시장 조회용 사라질 예정임
- private final Queue tradeQueue = new LinkedList<>(); //테스트를 위한 큐 -> 체결 db에서 데이터 조회
- private int maxQueueSize = 10;
-
- private double totalPriceVolume = 0;
- private double totalVolume = 0;
-
- 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));
- totalPriceVolume += price * volume;
- totalVolume += volume;
-// System.out.println("=== 최신 체결 큐 확인 : " + tradeQueue.peek().toString());
-// System.out.println("=== platformVWAP 변동 : "+getPlatformVWAP());
- }
-
- public double getPlatformVWAP() {
- return totalVolume == 0 ? 0.0 : totalPriceVolume / totalVolume;
+ Map vwapMap = new ConcurrentHashMap<>();
+
+ public double calculateVWAPbyTrades(String ticker,List trades,double apiVWAP) {
+ VWAPState state = vwapMap.computeIfAbsent(ticker, VWAPState::new);
+ if (trades.size() < 10){
+ //체결 내역이 10개 이하일 경우 자체 계산
+ return generateVWAP(apiVWAP);
+ }
+ state.calculateVWAPbyTrades(trades);
+ return state.getVWAP();
}
- public double calculateVWAPbyTrades(List trades) {
- for (Trade trade : trades) {
- double price = trade.getPrice();
- double volume = trade.getSize();
- if (volume <= 0) continue;
- totalPriceVolume += price * volume;
- totalVolume += volume;
-
+ public double generateVWAP ( double apiVWAP){
+ double maxDeviationaRate = 0.001; //보정값 0.1%만
+ double deviation = (Math.random() * 2 - 1) * maxDeviationaRate; //편차 계산
+ return apiVWAP * (1 + deviation); // +=deviation 난수 생성 후 계산 (범위는 -1~+1)
}
- return getPlatformVWAP();
- }
-
- 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 +
- '}';
- }
- }
-}
-
+ }
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/TickParser.java b/src/main/java/com/cleanengine/coin/realitybot/service/TickParser.java
new file mode 100644
index 00000000..c82f6e84
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/realitybot/service/TickParser.java
@@ -0,0 +1,29 @@
+package com.cleanengine.coin.realitybot.service;
+
+import com.cleanengine.coin.realitybot.dto.Ticks;
+import com.google.gson.Gson;
+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 TickParser {
+ private static final Gson gson = new Gson();
+
+ public static List parseGson(String json) {
+ return gson.fromJson(json, new TypeToken>() {}.getType());
+ }
+ /*
+ * TickService의 목적을 분류하여 api 로 받아온 값을 parsing 과 api vwap을 계산하는 로직으로 분류하였습니다.
+ * 따라서 processVWAP과 CalculateVWAP은 제거되었으며
+ * 해당 클래스는 TickParser로 명칭을 변경하였습니다.
+ * */
+
+}
diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/TickService.java b/src/main/java/com/cleanengine/coin/realitybot/service/TickService.java
deleted file mode 100644
index 8c43bcc1..00000000
--- a/src/main/java/com/cleanengine/coin/realitybot/service/TickService.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.cleanengine.coin.realitybot.service;
-
-import com.cleanengine.coin.realitybot.dto.Ticks;
-import com.google.gson.Gson;
-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;
-import java.util.Queue;
-
-@Service
-@RequiredArgsConstructor
-@Slf4j
-@Getter
-public class TickService implements TicketServiceInterface {
- private static final Gson gson = new Gson();//이거 왜 static으로? service에서 받아오면 되는데
- private final Queue ticksQueue;
-
- private double vwap;
- private double totalPriceVolume;
- private double totalVolume;
-
- public static List paraseGson(String json) {
- return gson.fromJson(json, new TypeToken>() {}.getType());
- }
-
- public void processVWAP(){ //vwap 구하기 실행
-// System.out.println("매서드 실행 시");
- if (ticksQueue.size()<10) {return;} //10개 이상일 경우 실행
- vwap = calculateVWAP(ticksQueue); //vwap 계산 실행
-
-// log.info("현재 VWAP: {}",vwap);
-// System.out.println("=== 현재 VWAP 가격 "+vwap);
- }
-
- @Override
- public double calculateVWAP(Queue ticksQueue) { //vwap (거래량 가중 평균가) 계산기
- //누적 거래 금액 (가격 * 거래량)
- totalPriceVolume = 0.0;
- // 누적 거래량을 저장
- totalVolume = 0.0;
-
- //queue 순회하여 각 틱당 가격과 거래량 누적 합산
- for (Ticks ticks : ticksQueue) {
- totalPriceVolume += ticks.getTrade_price() * ticks.getTrade_volume();//거래 금액 누적
- totalVolume += ticks.getTrade_volume();//거래량 누적
-
- /*//모니터링용 코드
- log.info("추가 된 가격 : {}",ticks.getTrade_price());
- log.info("추가 된 아이디 : {}",ticks.getSequential_id());
- */
- }
- //0 은 오류 방지
- //VWAP 반환
- return totalVolume == 0? 0.0: totalPriceVolume / totalVolume;
-
- }
-
-}
diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/TickServiceManager.java b/src/main/java/com/cleanengine/coin/realitybot/service/TickServiceManager.java
new file mode 100644
index 00000000..a49ab9c6
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/realitybot/service/TickServiceManager.java
@@ -0,0 +1,18 @@
+package com.cleanengine.coin.realitybot.service;
+
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class TickServiceManager {
+ /*해당 코드는 ticker 별로 apiVWAP을 계산하기 위해 만들어진 코드입니다.
+ * 초기엔 전역에서 vwap을 계산하거나 sequentialid를 변수에 담았으나 인스턴스가 종목별로 생성되어야 해서 작성되었습니다.
+ * ConcurrentHashMap을 통해 중복 검사 후 종목명으로 만들어진 게 없다면 새로 만듭니다.
+ * */
+ private final Map tickServiceMap = new ConcurrentHashMap<>();
+ public ApiVWAPService getService(String ticker) {
+ return tickServiceMap.computeIfAbsent(ticker, t -> new ApiVWAPService());
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/TicketServiceInterface.java b/src/main/java/com/cleanengine/coin/realitybot/service/TicketServiceInterface.java
deleted file mode 100644
index fd73a858..00000000
--- a/src/main/java/com/cleanengine/coin/realitybot/service/TicketServiceInterface.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.cleanengine.coin.realitybot.service;
-
-
-import com.cleanengine.coin.realitybot.dto.Ticks;
-
-import java.util.Queue;
-
-public interface TicketServiceInterface {
- double calculateVWAP(Queue ticksQueue);
-}
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 bbf68b5d..289144e5 100644
--- a/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java
+++ b/src/main/java/com/cleanengine/coin/realitybot/service/VWAPerrorInJectionScheduler.java
@@ -3,10 +3,14 @@
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 {
@@ -14,27 +18,31 @@ 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 = 60000) // 혹은 따로 수동 호출도 가능
+ @Scheduled(fixedRate = 60000) // 혹은 따로 수동 호출도 가능
public void injectFakeTrade() {
- if (!shouldInject) return;
+ if (!shouldInject || hasInjected) return;
Trade fakeTrade = new Trade();
fakeTrade.setTicker("TRUMP");
- fakeTrade.setBuyUserId(9999); // 테스트용 유저 ID
- fakeTrade.setSellUserId(9998); // 테스트용 유저 ID
+ 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(3000.0); // 대량 체결
+ fakeTrade.setSize(300.0); // 대량 체결
// fakeTrade.setSize(100.0); // 대량 체결
fakeTrade.setTradeTime(LocalDateTime.now());
tradeRepository.save(fakeTrade);
+ hasInjected = true; // ✅ 한 번 실행했으니 플래그 설정
shouldInject = false;
-// System.out.println("🚨 혼동 Trade 1건 삽입 완료!");
+ System.out.println("🚨 혼동 Trade 1건 삽입 완료!");
}
}
diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/VirtualMarketService.java b/src/main/java/com/cleanengine/coin/realitybot/service/VirtualMarketService.java
deleted file mode 100644
index b6dc42e6..00000000
--- a/src/main/java/com/cleanengine/coin/realitybot/service/VirtualMarketService.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.cleanengine.coin.realitybot.service;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.util.Random;
-
-@Service
-@Slf4j
-public class VirtualMarketService {//이거 안씀이젬
- private final Random random = new Random();
- private final TickService tickService;
-
- //1회용 으로 전환
- private boolean initialized = false;
- private double initialVWAP;
-
- public VirtualMarketService(TickService tickService) {
- this.tickService = tickService;
- }
-
-// public double switcherVWAP(Queue ticksQueue, double platformVWAP){
-// if (!initialized || platformVWAP == 0.0) {
-// initialVWAP = getVirtualMarketPrice(ticksQueue);
-// initialized = true;
-// log.info("초기 VWAP 생성: {}", initialVWAP);
-// return initialVWAP;
-// } else {
-// log.info("초기 VWAP 생성: {}", initialVWAP);
-// log.info("vwap 생성 : {}",platformVWAP);
-// return platformVWAP;
-// }
-// }
-
-// public double getVirtualMarketPrice(Queue ticksQueue) { //10개로 제한 된 ticks를 받음
-// double realVWAP = tickService.calculateVWAP(ticksQueue);//다시 평균가 계산 : 또 계산할 필요가 있을까?
-// double adjustment = getRandomAdjustment(realVWAP); //랜덤 조정 : +-0.5% 범위 내 랜덤 값 부여
-//
-// double virtualPrice = realVWAP + adjustment; // 최종 가격
-//
-// /*//모니터링 (가상 = trade 체결금액에서 vwap)만 구하면 됨
-// log.info("현실 VWAP : {}, 보정치 : {}, 가상 VWAP : {}", realVWAP, adjustment, virtualPrice);
-// */
-//
-// return virtualPrice;
-// }
-
-// private double getRandomAdjustment(double realVWAP) { //가상 VWAP 제작용 랜덤 값 부여
-// double maxDeviationRate = 0.005; //(0.5%는 보정값)
-// double deviation = realVWAP * maxDeviationRate;//편차 계산
-//
-// return (random.nextDouble()*2-1)* deviation; // +=deviation 난수 생성 후 계산 (범위는 -1~+1)
-// }
-}
diff --git a/src/main/java/com/cleanengine/coin/realitybot/service/VirtualTradeService.java b/src/main/java/com/cleanengine/coin/realitybot/service/VirtualTradeService.java
index 41bd11ed..a32983d5 100644
--- a/src/main/java/com/cleanengine/coin/realitybot/service/VirtualTradeService.java
+++ b/src/main/java/com/cleanengine/coin/realitybot/service/VirtualTradeService.java
@@ -1,11 +1,10 @@
package com.cleanengine.coin.realitybot.service;
import com.cleanengine.coin.realitybot.dto.TestOrder;
-import org.springframework.stereotype.Service;
import java.util.*;
-@Service
+//@Service
public class VirtualTradeService {
private final OrderQueueManagerService queueManager;
private final PlatformVWAPService platformVWAPService;
@@ -14,8 +13,10 @@ public VirtualTradeService(OrderQueueManagerService queueManager, PlatformVWAPSe
this.platformVWAPService = platformVWAPService;
this.queueManager = queueManager;
}
+ /*해당 코드는 초기 개별 모듈로 작업할 때 가상의 체결을 만드는 코드였습니다.
+ * 이젠 쓰이지 않는 코드이나 어떤 에러가 발생할 때 재사용하기 위한 용도로 삭제하지 않았습니다.
+ * */
- //todo 비활성화 예정 BUT recordTrade()를 받아와야함.
//가상 주문 매칭 및 체결 처리를 담당하는 서비스
public void matchOrder(){
//매수, 매도 주문 큐 관리
@@ -26,10 +27,6 @@ public void matchOrder(){
//주문 추출
TestOrder buyOrder = buyQueue.peek(); //가장 높은 매수 주문
TestOrder sellOrder = sellQueue.peek(); // 가장 낮은 매도 주문
- /*//모니터링용
- System.out.println("=== queue확인 - 매수큐 가격 : "+buyOrder.getPrice()+", 수량 : "+buyOrder.getVolume()+"===");
- System.out.println("=== queue확인 - 매도큐 가격 : "+sellOrder.getPrice()+", 수량 : "+sellOrder.getVolume()+"===");
-*/
//체결 조건 부여 : 현재 느슨한 체결 (1:1은 문제 발생/어짜피 매서드 호출 힘너무 쓰면 안됨)
//매수 희망가 >= 매도 희망가
@@ -51,9 +48,8 @@ public void matchOrder(){
if (buyOrder.getVolume() <= 0) buyQueue.poll();
if (sellOrder.getVolume() <= 0) sellQueue.poll();
- //VWAP 계산을 위한 거래 기록 TODO JPA로 받아온 값이 들어가야 함
- platformVWAPService.recordTrade(matchedPrice,matchedVolume);
- } //쌓이긴 하는데 100원따리로 쌓임 , generateorder가 0원 받을 때 해결해야 함.
+// platformVWAPService.recordTrade(matchedPrice,matchedVolume);
+ }
else {
break;
}
@@ -77,9 +73,6 @@ public void matchOrderbyIterator(){
if ((int)buyOrder.getPrice() >= (int)sellOrder.getPrice()){
double matchVolume = Math.min(buyOrder.getVolume(), sellOrder.getVolume());
if (matchVolume <=0) continue;
-// System.out.printf("=== 체결 완료 : %.1f / %.4f \n",sellOrder.getPrice(),matchVolume);
- platformVWAPService.recordTrade(sellOrder.getPrice(),matchVolume);
-
buyOrder.setVolume(buyOrder.getVolume() - matchVolume);
sellOrder.setVolume(sellOrder.getVolume() - matchVolume);
@@ -95,7 +88,6 @@ public void matchOrderbyIterator(){
}
}
-// System.out.println("===exctued 값 buy : "+excutedBuy+"sell"+excutedSell);
queueManager.getBuyqueue().removeAll(excutedBuy);
queueManager.getSellqueue().removeAll(excutedSell);
diff --git a/src/main/java/com/cleanengine/coin/realitybot/vo/APITicker.java b/src/main/java/com/cleanengine/coin/realitybot/vo/APITicker.java
new file mode 100644
index 00000000..04458350
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/realitybot/vo/APITicker.java
@@ -0,0 +1,14 @@
+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/VWAPState.java b/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java
new file mode 100644
index 00000000..8f7e6e7f
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/realitybot/vo/VWAPState.java
@@ -0,0 +1,78 @@
+package com.cleanengine.coin.realitybot.vo;
+
+import com.cleanengine.coin.trade.entity.Trade;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+@Getter
+@Setter
+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) {
+
+// 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));
+ 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);
+// 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 +
+ '}';
+ }
+ }
+}
diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeBatchProcessor.java b/src/main/java/com/cleanengine/coin/trade/application/TradeBatchProcessor.java
index 0bb75ba2..77a34382 100644
--- a/src/main/java/com/cleanengine/coin/trade/application/TradeBatchProcessor.java
+++ b/src/main/java/com/cleanengine/coin/trade/application/TradeBatchProcessor.java
@@ -1,7 +1,7 @@
package com.cleanengine.coin.trade.application;
import com.cleanengine.coin.chart.dto.TradeEventDto;
-import com.cleanengine.coin.order.application.queue.OrderQueueManagerPool;
+import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager;
import com.cleanengine.coin.orderbook.application.service.UpdateOrderBookUsecase;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
@@ -10,6 +10,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@@ -21,11 +22,12 @@
import java.util.concurrent.TimeUnit;
@Service
+@Order(4)
public class TradeBatchProcessor implements ApplicationRunner {
Logger logger = LoggerFactory.getLogger(TradeBatchProcessor.class);
- private final OrderQueueManagerPool orderQueueManagerPool;
+ private final WaitingOrdersManager waitingOrdersManager;
private final TradeService tradeService;
private final List executors = new ArrayList<>();
@@ -35,8 +37,8 @@ public class TradeBatchProcessor implements ApplicationRunner {
@Value("${order.tickers}") String[] tickers;
- public TradeBatchProcessor(OrderQueueManagerPool orderQueueManagerPool, TradeService tradeService, UpdateOrderBookUsecase updateOrderBookUsecase) {
- this.orderQueueManagerPool = orderQueueManagerPool;
+ public TradeBatchProcessor(WaitingOrdersManager waitingOrdersManager, TradeService tradeService, UpdateOrderBookUsecase updateOrderBookUsecase) {
+ this.waitingOrdersManager = waitingOrdersManager;
this.tradeService = tradeService;
this.updateOrderBookUsecase = updateOrderBookUsecase;
}
@@ -48,7 +50,7 @@ public void run(ApplicationArguments args) {
private void processTrades() {
for (String ticker : tickers) {
- TradeQueueManager tradeQueueManager = new TradeQueueManager(orderQueueManagerPool.getOrderQueueManager(ticker),
+ TradeQueueManager tradeQueueManager = new TradeQueueManager(waitingOrdersManager.getWaitingOrders(ticker),
updateOrderBookUsecase,
tradeService);
tradeQueueManagers.put(ticker, tradeQueueManager); // 정상 종료를 위해 저장
@@ -95,19 +97,9 @@ public void shutdown() {
}
}
+ @Deprecated
public TradeEventDto retrieveTradeEventDto(String ticker) {
- TradeQueueManager tradeQueueManager = this.tradeQueueManagers.get(ticker);
- if (tradeQueueManager == null) {
- return null;
- }
-
- TradeEventDto lastTradeEventDto = tradeQueueManager.getLastTradeEventDto();
-
- // 서비스 시작 후 체결 내역이 없으면 null 반환
- if (lastTradeEventDto.getSize() == 0.0 || lastTradeEventDto.getPrice() == 0.0) {
- return null;
- }
- return lastTradeEventDto;
+ return null;
}
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedEvent.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedEvent.java
new file mode 100644
index 00000000..11f90ebb
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedEvent.java
@@ -0,0 +1,13 @@
+package com.cleanengine.coin.trade.application;
+
+import com.cleanengine.coin.trade.entity.Trade;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class TradeExecutedEvent {
+ Trade trade;
+ Long buyOrderId;
+ Long sellOrderId;
+}
diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedEventPublisher.java b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedEventPublisher.java
new file mode 100644
index 00000000..7e6d5dbc
--- /dev/null
+++ b/src/main/java/com/cleanengine/coin/trade/application/TradeExecutedEventPublisher.java
@@ -0,0 +1,19 @@
+package com.cleanengine.coin.trade.application;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TradeExecutedEventPublisher {
+
+ private final ApplicationEventPublisher publisher;
+
+ public TradeExecutedEventPublisher(ApplicationEventPublisher publisher) {
+ this.publisher = publisher;
+ }
+
+ public void publish(TradeExecutedEvent event) {
+ publisher.publishEvent(event);
+ }
+
+}
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 6f7bdf44..a65af58d 100644
--- a/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java
+++ b/src/main/java/com/cleanengine/coin/trade/application/TradeQueueManager.java
@@ -1,81 +1,29 @@
package com.cleanengine.coin.trade.application;
-import com.cleanengine.coin.chart.dto.TradeEventDto;
-import com.cleanengine.coin.order.application.queue.OrderQueueManager;
-import com.cleanengine.coin.order.domain.BuyOrder;
-import com.cleanengine.coin.order.domain.Order;
-import com.cleanengine.coin.order.domain.SellOrder;
+import com.cleanengine.coin.order.domain.spi.WaitingOrders;
import com.cleanengine.coin.orderbook.application.service.UpdateOrderBookUsecase;
-import lombok.Getter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.time.LocalDateTime;
-import java.util.Optional;
-import java.util.concurrent.PriorityBlockingQueue;
+import lombok.extern.slf4j.Slf4j;
+@Slf4j
public class TradeQueueManager {
- private static final Logger logger = LoggerFactory.getLogger(TradeQueueManager.class);
- private long lastLogTime = 0;
- private static final long LOG_INTERVAL = 1000;
- private final double MINIMUM_ORDER_SIZE = 0.00000001;
-
private volatile boolean running = true; // 무한루프 종료 플래그
- @Getter
- private final OrderQueueManager orderQueueManager;
private final String ticker;
- private final PriorityBlockingQueue marketSellOrderQueue;
- private final PriorityBlockingQueue limitSellOrderQueue;
- private final PriorityBlockingQueue marketBuyOrderQueue;
- private final PriorityBlockingQueue limitBuyOrderQueue;
-
- private final UpdateOrderBookUsecase updateOrderBookUsecase;
-
private final TradeService tradeService;
- @Getter
- private final TradeEventDto lastTradeEventDto;
-
- public TradeQueueManager(OrderQueueManager orderQueueManager, UpdateOrderBookUsecase updateOrderBookUsecase, TradeService tradeService) {
- this.orderQueueManager = orderQueueManager;
- this.updateOrderBookUsecase = updateOrderBookUsecase;
+ public TradeQueueManager(WaitingOrders waitingOrders, UpdateOrderBookUsecase updateOrderBookUsecase, TradeService tradeService) {
this.tradeService = tradeService;
- // TODO : orderQueueManager의 필드를 꺼내서 쓰는 게 맞는 방식인지 고려
- this.ticker = orderQueueManager.getTicker();
- this.marketSellOrderQueue = orderQueueManager.getMarketSellOrderQueue();
- this.limitSellOrderQueue = orderQueueManager.getLimitSellOrderQueue();
- this.marketBuyOrderQueue = orderQueueManager.getMarketBuyOrderQueue();
- this.limitBuyOrderQueue = orderQueueManager.getLimitBuyOrderQueue();
-
- lastTradeEventDto = new TradeEventDto();
- lastTradeEventDto.setTicker(ticker);
+ this.ticker = waitingOrders.getTicker();
}
public void run() {
- /*
- 문제 - 큐가 비어있는데 굳이 일을해야할까?
- - 큐가 안비어있을떄는 특정 스레드를 호출해서 일을 시키고, 아니면 스레드 블락킹(대기큐)
- -> BQ 메커니즘
- (참고) queueManager.getMarketSellOrderQueue().notify();
- BlockingQueue 에 데이터가 없으면 스레드가 블록되는데, 이걸 활용해서 구현?
- 시장가 처리는 큐의 데이터 존재 여부를 통해 할 수 있겠지만..
- 비어있을 때가 기준이 아닌, 새로운 주문이 요청되었을 때 한 번 순회하는 게 이상적
- */
+ // TODO : 주문 시 이벤트 기반으로 동작하도록 개선
while (running) {
try {
- Optional> targetTradePair = this.matchOrders();
- if (targetTradePair.isEmpty()) {
- continue;
- } else {
- this.executeTrade(targetTradePair.get().getBuyOrder(), targetTradePair.get().getSellOrder());
- }
-// Thread.sleep(10);
-// } catch (InterruptedException e) {
+ tradeService.execMatchAndTrade(ticker);
} catch (Exception e) {
- logger.error("Error processing trades for {}: {}", this.ticker, e.getMessage());
- throw e;
+ log.error("Error processing trades for {}: {}", this.ticker, e.getMessage());
}
}
}
@@ -84,210 +32,4 @@ public void stop() {
this.running = false; // 무한루프 종료 플래그
}
- private Optional> matchOrders() { // 반환값 : 체결여부
- this.writeQueueLog();
- TradePair targetTradePair = null;
-
- // 시장가 주문 우선처리
- // TODO : race condition 방지 방안? (isEmpty <-> peek 간격 발생)
- if (hasElement(this.marketSellOrderQueue) && hasElement(this.limitBuyOrderQueue)) {
- // 1. 시장가 매도 주문, 지정가 매수 주문
- SellOrder marketSellOrder = this.marketSellOrderQueue.peek();
- BuyOrder limitBuyOrder = this.limitBuyOrderQueue.peek();
- targetTradePair = new TradePair<>(marketSellOrder, limitBuyOrder);
- } else if (hasElement(this.marketBuyOrderQueue) && hasElement(this.limitSellOrderQueue)) {
- // 2. 시장가 매수 주문, 지정가 매도 주문
- BuyOrder marketBuyOrder = this.marketBuyOrderQueue.peek();
- SellOrder limitSellOrder = this.limitSellOrderQueue.peek();
- targetTradePair = new TradePair<>(marketBuyOrder, limitSellOrder);
- } else if (hasElement(this.limitSellOrderQueue) && hasElement(this.limitBuyOrderQueue)) {
- // 3. 지정가 주문
- targetTradePair = this.matchBetweenLimitOrders();
- }
- return Optional.ofNullable(targetTradePair);
- }
-
- private void writeQueueLog() {
- long currentTime = System.currentTimeMillis();
- if (currentTime - lastLogTime > LOG_INTERVAL) {
- logger.debug("주문 큐 - 시장가매도[{}], 지정가매도[{}], 시장가매수[{}], 지정가매수[{}]",
- this.marketSellOrderQueue.size(),
- this.limitSellOrderQueue.size(),
- this.marketBuyOrderQueue.size(),
- this.limitBuyOrderQueue.size());
- lastLogTime = currentTime;
- }
- }
-
- private TradePair matchBetweenLimitOrders() {
- PriorityBlockingQueue limitSellQueue = this.limitSellOrderQueue;
- PriorityBlockingQueue limitBuyQueue = this.limitBuyOrderQueue;
-
- if (this.hasElement(limitSellQueue) && this.hasElement(limitBuyQueue)) {
- SellOrder sellOrder = limitSellQueue.peek();
- BuyOrder buyOrder = limitBuyQueue.peek();
-
- if (this.canMatch(buyOrder, sellOrder)) {
- return new TradePair<>(buyOrder, sellOrder);
- } else
- return null;
- } else
- return null;
- }
-
- private boolean hasElement(PriorityBlockingQueue extends Order> queue) {
- return !queue.isEmpty();
- }
-
- private boolean canMatch(BuyOrder buyOrder, SellOrder sellOrder) {
- if (buyOrder == null || sellOrder == null)
- return false;
- return buyOrder.getPrice() >= sellOrder.getPrice();
- }
-
- protected void executeTrade(BuyOrder buyOrder, SellOrder sellOrder) {
- this.writeTradingLog(buyOrder, sellOrder);
- // 주문 관련 처리를 먼저 해서 동기화 이슈 가능성을 최소로 하고, 체결 내역 처리는 별도 스레드로 분리하는 게 낫겠다
- double tradedPrice;
- double tradedSize;
- double totalTradedPrice;
-
- // 체결 단가, 수량 확정
- TradeUnitPriceAndSize tradeUnitPriceAndSize = getTradeUnitPriceAndSize(buyOrder, sellOrder);
- tradedSize = tradeUnitPriceAndSize.tradedSize();
- tradedPrice = tradeUnitPriceAndSize.tradedPrice();
- if (tradedSize < MINIMUM_ORDER_SIZE) {
- return;
- }
- totalTradedPrice = tradedPrice * tradedSize;
-
- // 주문 잔여수량, 잔여금액 감소
- if (isMarketOrder(buyOrder))
- buyOrder.decreaseRemainingDeposit(totalTradedPrice);
- else
- buyOrder.decreaseRemainingSize(tradedSize);
- sellOrder.decreaseRemainingSize(tradedSize);
-
- // 주문 완전체결 처리(잔여금액 or 잔여수량이 0)
- this.removeCompletedBuyOrder(buyOrder);
- this.removeCompletedSellOrder(sellOrder);
-
- // DB 테이블 저장에 걸리는 시간 측정용
- long beforeTime = System.currentTimeMillis();
- tradeService.saveOrder(buyOrder);
- tradeService.saveOrder(sellOrder);
- long afterTime = System.currentTimeMillis();
- logger.debug("주문 테이블에 update하는 데 걸린 시간 : {}ms", afterTime - beforeTime);
-
- // 예수금 처리
- // - 매수 잔여금액 반환
- if (isMarketOrder(buyOrder)) {
- ; // TODO : 시장가 거래 시 1원 단위 등 작은 금액이 남을 수도 있는데 처리방안
- } else {
- if (buyOrder.getPrice() - tradedPrice > MINIMUM_ORDER_SIZE) { // 매도 호가보다 높은 가격에 매수를 시도한 경우, 차액 반환
- double totalRefundAmount = (buyOrder.getPrice() - tradedPrice) * tradedSize;
- if (totalRefundAmount > MINIMUM_ORDER_SIZE)
- tradeService.increaseAccountCash(buyOrder, totalRefundAmount);
- }
- }
-
- // - 매도 예수금 처리
- tradeService.increaseAccountCash(sellOrder, totalTradedPrice);
-
- // 지갑 누적계산
- tradeService.updateWalletAfterTrade(buyOrder, this.ticker, tradedSize, totalTradedPrice);
- tradeService.updateWalletAfterTrade(sellOrder, this.ticker, tradedSize, totalTradedPrice);
-
- // 마지막 체결내역 임시 보관(웹소켓 전송용)
- lastTradeEventDto.setSize(tradedSize);
- lastTradeEventDto.setPrice(tradedPrice);
- lastTradeEventDto.setTimestamp(LocalDateTime.now());
-
- // 체결내역 저장
- tradeService.insertNewTrade(this.ticker, buyOrder, sellOrder, tradedSize, tradedPrice);
-
- // 호가 조회를 위한 Order Service 메서드 호출
- updateOrderBookUsecase.updateOrderBookOnTradeExecuted(ticker, buyOrder.getId(), sellOrder.getId(), tradedSize);
-
- // TODO: DB처리는 트랜잭션 묶고, 이후 큐 보상 트랜잭션 처리 코드 작성
- }
-
- private static TradeUnitPriceAndSize getTradeUnitPriceAndSize(BuyOrder buyOrder, SellOrder sellOrder) {
- double tradedPrice;
- double tradedSize;
- if (isMarketOrder(buyOrder)) { // 시장가매수-지정가매도
- tradedPrice = sellOrder.getPrice();
- if (buyOrder.getRemainingDeposit() >= tradedPrice * sellOrder.getRemainingSize()) { // 매수 잔여예수금이 매도 잔여량보다 크거나 같은 경우 (매수 부분체결 or 완전체결, 매도 완전체결)
- tradedSize = sellOrder.getRemainingSize();
- } else {
- tradedSize = buyOrder.getRemainingDeposit() / tradedPrice;
- }
- } else if (isMarketOrder(sellOrder)) { // 시장가매도-지정가매수
- tradedPrice = buyOrder.getPrice();
- tradedSize = Math.min(sellOrder.getRemainingSize(), buyOrder.getRemainingSize());
- } else { // 지정가매수-지정가매도
- tradedPrice = getTradedUnitPrice(buyOrder, sellOrder);
- tradedSize = Math.min(buyOrder.getRemainingSize(), sellOrder.getRemainingSize());
- }
- TradeUnitPriceAndSize tradeUnitPriceAndSize = new TradeUnitPriceAndSize(tradedSize, tradedPrice);
- return tradeUnitPriceAndSize;
- }
-
- private record TradeUnitPriceAndSize(double tradedSize, double tradedPrice) {
- }
-
- private static Boolean isMarketOrder(Order order) {
- return order.getIsMarketOrder();
- }
-
- private static Boolean isLimitOrder(Order order) {
- return !order.getIsMarketOrder();
- }
-
- private void writeTradingLog(BuyOrder buyOrder, SellOrder sellOrder) {
- logger.debug("[{}] 체결 확정! 종목: {}, ({}: {}가 {}로 {}만큼 매수주문), ({}: {}가 {}로 {}만큼 매도주문)",
- Thread.currentThread().threadId(),
- buyOrder.getTicker(),
- buyOrder.getId(),
- buyOrder.getUserId(),
- isMarketOrder(buyOrder) ? "시장가" : "지정가(" + buyOrder.getPrice() + "원)",
- buyOrder.getRemainingSize() == null ? buyOrder.getRemainingDeposit() : buyOrder.getRemainingSize(),
- sellOrder.getId(),
- sellOrder.getUserId(),
- isMarketOrder(sellOrder) ? "시장가" : "지정가(" + sellOrder.getPrice() + "원)",
- sellOrder.getRemainingSize());
- }
-
- private void removeCompletedBuyOrder(BuyOrder order) {
- boolean isOrderCompleted = (isMarketOrder(order) && order.getRemainingDeposit() < MINIMUM_ORDER_SIZE) ||
- (isLimitOrder(order) && order.getRemainingSize() < MINIMUM_ORDER_SIZE);
-
- if (isOrderCompleted) {
- PriorityBlockingQueue extends Order> orderQueue =
- isMarketOrder(order) ? this.marketBuyOrderQueue : this.limitBuyOrderQueue;
- orderQueue.remove(order);
- tradeService.updateCompletedOrderStatus(order);
- }
- }
-
- private void removeCompletedSellOrder(SellOrder order) {
- boolean isOrderCompleted = order.getRemainingSize() < MINIMUM_ORDER_SIZE;
-
- if (isOrderCompleted) {
- PriorityBlockingQueue extends Order> orderQueue =
- order.getIsMarketOrder() ? this.marketSellOrderQueue : this.limitSellOrderQueue;
- orderQueue.remove(order);
- tradeService.updateCompletedOrderStatus(order);
- }
- }
-
- private static double getTradedUnitPrice(BuyOrder buyOrder, SellOrder sellOrder) {
- // 주문 시간을 비교하여 먼저 들어온 주문의 가격으로 거래
- if (buyOrder.getCreatedAt().isBefore(sellOrder.getCreatedAt())) {
- return buyOrder.getPrice();
- } else {
- return sellOrder.getPrice();
- }
- }
-
}
\ No newline at end of file
diff --git a/src/main/java/com/cleanengine/coin/trade/application/TradeService.java b/src/main/java/com/cleanengine/coin/trade/application/TradeService.java
index 12ca4330..5ffe6609 100644
--- a/src/main/java/com/cleanengine/coin/trade/application/TradeService.java
+++ b/src/main/java/com/cleanengine/coin/trade/application/TradeService.java
@@ -1,26 +1,30 @@
package com.cleanengine.coin.trade.application;
-import com.cleanengine.coin.chart.dto.TradeEventDto;
import com.cleanengine.coin.common.error.BusinessException;
import com.cleanengine.coin.common.response.ErrorStatus;
-import com.cleanengine.coin.order.domain.BuyOrder;
-import com.cleanengine.coin.order.domain.Order;
-import com.cleanengine.coin.order.domain.OrderStatus;
-import com.cleanengine.coin.order.domain.SellOrder;
-import com.cleanengine.coin.order.infra.BuyOrderRepository;
-import com.cleanengine.coin.order.infra.SellOrderRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.order.command.BuyOrderRepository;
+import com.cleanengine.coin.order.adapter.out.persistentce.order.command.SellOrderRepository;
+import com.cleanengine.coin.order.domain.*;
+import com.cleanengine.coin.order.domain.spi.WaitingOrders;
+import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager;
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 com.cleanengine.coin.user.info.infra.AccountRepository;
import com.cleanengine.coin.user.info.infra.WalletRepository;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
-import java.util.Map;
import java.util.Optional;
+import static com.cleanengine.coin.common.CommonValues.approxEquals;
+
+@Slf4j
@Service
+@Transactional
public class TradeService {
private final TradeRepository tradeRepository;
@@ -28,13 +32,22 @@ public class TradeService {
private final SellOrderRepository sellOrderRepository;
private final AccountRepository accountRepository;
private final WalletRepository walletRepository;
+ @Getter
+ private final WaitingOrdersManager waitingOrdersManager;
+ private final TradeExecutedEventPublisher tradeExecutedEventPublisher;
+
+ // 1초마다 큐 로깅
+ private long lastLogTime = 0;
+ private static final long LOG_INTERVAL = 1000;
- public TradeService(TradeRepository tradeRepository, BuyOrderRepository buyOrderRepository, SellOrderRepository sellOrderRepository, AccountRepository accountRepository, WalletRepository walletRepository) {
+ public TradeService(TradeRepository tradeRepository, BuyOrderRepository buyOrderRepository, SellOrderRepository sellOrderRepository, AccountRepository accountRepository, WalletRepository walletRepository, WaitingOrdersManager waitingOrdersManager, TradeExecutedEventPublisher tradeExecutedEventPublisher) {
this.tradeRepository = tradeRepository;
this.buyOrderRepository = buyOrderRepository;
this.sellOrderRepository = sellOrderRepository;
this.accountRepository = accountRepository;
this.walletRepository = walletRepository;
+ this.waitingOrdersManager = waitingOrdersManager;
+ this.tradeExecutedEventPublisher = tradeExecutedEventPublisher;
}
public Trade saveTrade(Trade trade) {
@@ -85,7 +98,7 @@ public Wallet findWalletByUserIdAndTicker(Integer userId, String ticker) {
.orElseGet(() -> createNewWallet(account.getId(), ticker));
}
- private Wallet createNewWallet(Integer accountId, String ticker) {
+ public Wallet createNewWallet(Integer accountId, String ticker) {
Wallet newWallet = new Wallet();
newWallet.setAccountId(accountId);
newWallet.setTicker(ticker);
@@ -114,4 +127,201 @@ public void updateCompletedOrderStatus(Order order) {
order.setState(OrderStatus.DONE);
}
+ private void writeQueueLog(WaitingOrders waitingOrders) {
+ long currentTime = System.currentTimeMillis();
+ if (currentTime - lastLogTime > LOG_INTERVAL) {
+ log.debug("주문 큐 - 시장가매도[{}], 지정가매도[{}], 시장가매수[{}], 지정가매수[{}]",
+ waitingOrders.getSellOrderPriorityQueueStore(OrderType.MARKET).size(),
+ waitingOrders.getSellOrderPriorityQueueStore(OrderType.LIMIT).size(),
+ waitingOrders.getBuyOrderPriorityQueueStore(OrderType.MARKET).size(),
+ waitingOrders.getBuyOrderPriorityQueueStore(OrderType.LIMIT).size());
+ lastLogTime = currentTime;
+ }
+ }
+
+ public void execMatchAndTrade(String ticker) {
+ WaitingOrders waitingOrders = waitingOrdersManager.getWaitingOrders(ticker);
+ // TODO : peek() 해온 Order 객체들을 lock -> 체결 도중 취소 방지
+ this.matchOrders(waitingOrders)
+ .ifPresent(tradePair -> executeTrade(waitingOrders, tradePair, ticker));
+ }
+
+ private Optional> matchOrders(WaitingOrders waitingOrders) { // 반환값 : 체결여부
+ this.writeQueueLog(waitingOrders);
+
+ TradePair targetTradePair;
+
+ // 시장가 주문 우선처리
+ SellOrder marketSellOrder = waitingOrders.getSellOrderPriorityQueueStore(OrderType.MARKET).peek();
+ SellOrder limitSellOrder = waitingOrders.getSellOrderPriorityQueueStore(OrderType.LIMIT).peek();
+ BuyOrder marketBuyOrder = waitingOrders.getBuyOrderPriorityQueueStore(OrderType.MARKET).peek();
+ BuyOrder limitBuyOrder = waitingOrders.getBuyOrderPriorityQueueStore(OrderType.LIMIT).peek();
+
+ if (marketSellOrder != null && limitBuyOrder != null) {
+ // 1. 시장가 매도 주문, 지정가 매수 주문
+ targetTradePair = new TradePair<>(marketSellOrder, limitBuyOrder);
+ } else if (marketBuyOrder != null && limitSellOrder != null) {
+ // 2. 시장가 매수 주문, 지정가 매도 주문
+ targetTradePair = new TradePair<>(marketBuyOrder, limitSellOrder);
+ } else {
+ // 3. 지정가 주문
+ targetTradePair = this.matchBetweenLimitOrders(limitBuyOrder, limitSellOrder);
+ }
+ return Optional.ofNullable(targetTradePair);
+ }
+
+ private TradePair matchBetweenLimitOrders(BuyOrder limitBuyOrder, SellOrder limitSellOrder) {
+ if (limitSellOrder == null || limitBuyOrder == null)
+ return null;
+
+ if (this.canMatch(limitBuyOrder, limitSellOrder))
+ return new TradePair<>(limitBuyOrder, limitSellOrder);
+ else
+ return null;
+ }
+
+ private boolean canMatch(BuyOrder buyOrder, SellOrder sellOrder) {
+ return buyOrder.getPrice() >= sellOrder.getPrice();
+ }
+
+ private record TradeUnitPriceAndSize(double tradedSize, double tradedPrice) {
+ }
+
+ public void executeTrade(WaitingOrders waitingOrders, TradePair tradePair, String ticker) {
+ BuyOrder buyOrder = tradePair.getBuyOrder();
+ SellOrder sellOrder = tradePair.getSellOrder();
+
+ double tradedPrice;
+ double tradedSize;
+ double totalTradedPrice;
+
+ // 체결 단가, 수량 확정
+ TradeUnitPriceAndSize tradeUnitPriceAndSize = getTradeUnitPriceAndSize(buyOrder, sellOrder);
+ tradedSize = tradeUnitPriceAndSize.tradedSize();
+ tradedPrice = tradeUnitPriceAndSize.tradedPrice();
+ if (approxEquals(tradedSize, 0.0)) {
+ log.debug("체결 중단! 체결 시도 수량 : {}, 매수단가 : {}, 매도단가 : {}", tradedSize, buyOrder.getPrice(), sellOrder.getPrice());
+ return;
+ }
+ this.writeTradingLog(buyOrder, sellOrder);
+
+ totalTradedPrice = tradedPrice * tradedSize;
+ // 주문 잔여수량, 잔여금액 감소
+ if (isMarketOrder(buyOrder))
+ buyOrder.decreaseRemainingDeposit(totalTradedPrice);
+ else
+ buyOrder.decreaseRemainingSize(tradedSize);
+ sellOrder.decreaseRemainingSize(tradedSize);
+
+ // 주문 완전체결 처리(잔여금액 or 잔여수량이 0)
+ this.removeCompletedBuyOrder(waitingOrders, buyOrder);
+ this.removeCompletedSellOrder(waitingOrders, sellOrder);
+
+ // DB 테이블 저장에 걸리는 시간 측정용
+ long beforeTime = System.currentTimeMillis();
+ this.saveOrder(buyOrder);
+ this.saveOrder(sellOrder);
+ long afterTime = System.currentTimeMillis();
+ log.debug("주문 테이블에 update하는 데 걸린 시간 : {}ms", afterTime - beforeTime);
+
+ // 예수금 처리
+ // - 매수 잔여금액 반환
+ if (isMarketOrder(buyOrder)) {
+ ; // TODO : 시장가 거래 시 1원 단위 등 작은 금액이 남을 수도 있는데 처리방안
+ } else {
+ if (buyOrder.getPrice() > tradedPrice) { // 매도 호가보다 높은 가격에 매수를 시도한 경우, 차액 반환
+ double totalRefundAmount = (buyOrder.getPrice() - tradedPrice) * tradedSize;
+ this.increaseAccountCash(buyOrder, totalRefundAmount);
+ }
+ }
+
+ // - 매도 예수금 처리
+ this.increaseAccountCash(sellOrder, totalTradedPrice);
+
+ // 지갑 누적계산
+ this.updateWalletAfterTrade(buyOrder, ticker, tradedSize, totalTradedPrice);
+ this.updateWalletAfterTrade(sellOrder, ticker, tradedSize, totalTradedPrice);
+
+ // 체결내역 저장
+ Trade trade = this.insertNewTrade(ticker, buyOrder, sellOrder, tradedSize, tradedPrice);
+
+ TradeExecutedEvent tradeExecutedEvent =
+ TradeExecutedEvent.builder()
+ .trade(trade)
+ .buyOrderId(buyOrder.getId())
+ .sellOrderId(sellOrder.getId())
+ .build();
+ tradeExecutedEventPublisher.publish(tradeExecutedEvent);
+ }
+
+ private static TradeUnitPriceAndSize getTradeUnitPriceAndSize(BuyOrder buyOrder, SellOrder sellOrder) {
+ double tradedPrice;
+ double tradedSize;
+ if (isMarketOrder(buyOrder)) { // 시장가매수-지정가매도
+ tradedPrice = sellOrder.getPrice();
+ if (buyOrder.getRemainingDeposit() >= tradedPrice * sellOrder.getRemainingSize()) { // 매수 잔여예수금이 매도 잔여량보다 크거나 같은 경우 (매수 부분체결 or 완전체결, 매도 완전체결)
+ tradedSize = sellOrder.getRemainingSize();
+ } else {
+ tradedSize = buyOrder.getRemainingDeposit() / tradedPrice;
+ }
+ } else if (isMarketOrder(sellOrder)) { // 시장가매도-지정가매수
+ tradedPrice = buyOrder.getPrice();
+ tradedSize = Math.min(sellOrder.getRemainingSize(), buyOrder.getRemainingSize());
+ } else { // 지정가매수-지정가매도
+ tradedPrice = getTradedUnitPrice(buyOrder, sellOrder);
+ tradedSize = Math.min(buyOrder.getRemainingSize(), sellOrder.getRemainingSize());
+ }
+ return new TradeUnitPriceAndSize(tradedSize, tradedPrice);
+ }
+
+ private static double getTradedUnitPrice(BuyOrder buyOrder, SellOrder sellOrder) {
+ // 주문 시간을 비교하여 먼저 들어온 주문의 가격으로 거래
+ if (buyOrder.getCreatedAt().isBefore(sellOrder.getCreatedAt())) {
+ return buyOrder.getPrice();
+ } else {
+ return sellOrder.getPrice();
+ }
+ }
+
+ private void writeTradingLog(BuyOrder buyOrder, SellOrder sellOrder) {
+ log.debug("[{}] 체결 확정! 종목: {}, ({}: {}가 {}로 {}만큼 매수주문), ({}: {}가 {}로 {}만큼 매도주문)",
+ Thread.currentThread().threadId(),
+ buyOrder.getTicker(),
+ buyOrder.getId(),
+ buyOrder.getUserId(),
+ isMarketOrder(buyOrder) ? "시장가" : "지정가(" + buyOrder.getPrice() + "원)",
+ buyOrder.getRemainingSize() == null ? buyOrder.getRemainingDeposit() : buyOrder.getRemainingSize(),
+ sellOrder.getId(),
+ sellOrder.getUserId(),
+ isMarketOrder(sellOrder) ? "시장가" : "지정가(" + sellOrder.getPrice() + "원)",
+ sellOrder.getRemainingSize());
+ }
+
+ private static Boolean isMarketOrder(Order order) {
+ return order.getIsMarketOrder();
+ }
+
+ private static Boolean isLimitOrder(Order order) {
+ return !order.getIsMarketOrder();
+ }
+
+ private void removeCompletedBuyOrder(WaitingOrders waitingOrders, BuyOrder order) {
+ boolean isOrderCompleted = (isMarketOrder(order) && approxEquals(order.getRemainingDeposit(), 0.0)) ||
+ (isLimitOrder(order) && approxEquals(order.getRemainingSize(), 0.0));
+
+ if (isOrderCompleted) {
+ waitingOrders.removeOrder(order);
+ this.updateCompletedOrderStatus(order);
+ }
+ }
+
+ private void removeCompletedSellOrder(WaitingOrders waitingOrders, SellOrder order) {
+ boolean isOrderCompleted = approxEquals(order.getRemainingSize(), 0.0);
+
+ if (isOrderCompleted) {
+ waitingOrders.removeOrder(order);
+ this.updateCompletedOrderStatus(order);
+ }
+ }
+
}
diff --git a/src/main/java/com/cleanengine/coin/user/domain/Account.java b/src/main/java/com/cleanengine/coin/user/domain/Account.java
index 0c5ecb01..2af1d406 100644
--- a/src/main/java/com/cleanengine/coin/user/domain/Account.java
+++ b/src/main/java/com/cleanengine/coin/user/domain/Account.java
@@ -1,17 +1,13 @@
package com.cleanengine.coin.user.domain;
import jakarta.persistence.*;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
+import lombok.*;
@Setter
@Getter
@Entity
@Table(name = "account")
@NoArgsConstructor
-@AllArgsConstructor
public class Account {
@Id
@@ -25,6 +21,19 @@ public class Account {
@Column(name = "cash", nullable = false)
private Double cash;
+ @Builder
+ private Account(Integer userId, Double cash) {
+ this.userId = userId;
+ this.cash = cash;
+ }
+
+ public static Account of(Integer userId, Double cash) {
+ return Account.builder()
+ .userId(userId)
+ .cash(cash)
+ .build();
+ }
+
// Cash 증가
public Account increaseCash(Double amount) {
if (amount <= 0) {
diff --git a/src/main/java/com/cleanengine/coin/user/domain/Wallet.java b/src/main/java/com/cleanengine/coin/user/domain/Wallet.java
index 0e68256b..f189c134 100644
--- a/src/main/java/com/cleanengine/coin/user/domain/Wallet.java
+++ b/src/main/java/com/cleanengine/coin/user/domain/Wallet.java
@@ -34,4 +34,13 @@ public class Wallet {
@Column(name = "roi")
private Double roi; // Return on Investment (수익률)
+ public static Wallet generateEmptyWallet(String ticker, Integer accountId){
+ Wallet wallet = new Wallet();
+ wallet.setTicker(ticker);
+ wallet.setAccountId(accountId);
+ wallet.setSize(0.0);
+ wallet.setBuyPrice(0.0);
+ wallet.setRoi(0.0);
+ return wallet;
+ }
}
diff --git a/src/main/java/com/cleanengine/coin/user/info/application/AccountService.java b/src/main/java/com/cleanengine/coin/user/info/application/AccountService.java
index ba0ec138..6e1535ff 100644
--- a/src/main/java/com/cleanengine/coin/user/info/application/AccountService.java
+++ b/src/main/java/com/cleanengine/coin/user/info/application/AccountService.java
@@ -17,4 +17,9 @@ public Account retrieveAccountByUserId(Integer userId) {
return accountRepository.findByUserId(userId).orElse(null);
}
+ public Account createNewAccount(Integer userId, double cash) {
+ Account account = Account.of(userId, cash);
+ return accountRepository.save(account);
+ }
+
}
diff --git a/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java b/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java
index 24bac57b..fde4d463 100644
--- a/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java
+++ b/src/main/java/com/cleanengine/coin/user/login/application/CustomOAuth2UserService.java
@@ -1,13 +1,13 @@
package com.cleanengine.coin.user.login.application;
-import com.cleanengine.coin.user.domain.Account;
+import com.cleanengine.coin.common.CommonValues;
import com.cleanengine.coin.user.domain.OAuth;
import com.cleanengine.coin.user.domain.User;
+import com.cleanengine.coin.user.info.application.AccountService;
import com.cleanengine.coin.user.login.infra.CustomOAuth2User;
import com.cleanengine.coin.user.login.infra.KakaoResponse;
import com.cleanengine.coin.user.login.infra.OAuth2Response;
import com.cleanengine.coin.user.login.infra.UserOAuthDetails;
-import com.cleanengine.coin.user.info.infra.AccountRepository;
import com.cleanengine.coin.user.info.infra.OAuthRepository;
import com.cleanengine.coin.user.info.infra.UserRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
@@ -21,12 +21,12 @@ public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
private final OAuthRepository oAuthRepository;
- private final AccountRepository accountRepository;
+ private final AccountService accountService;
- public CustomOAuth2UserService(UserRepository userRepository, OAuthRepository oAuthRepository, AccountRepository accountRepository) {
+ public CustomOAuth2UserService(UserRepository userRepository, OAuthRepository oAuthRepository, AccountService accountService) {
this.userRepository = userRepository;
this.oAuthRepository = oAuthRepository;
- this.accountRepository = accountRepository;
+ this.accountService = accountService;
}
@Override
@@ -64,11 +64,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
newOAuth.setNickname(name);
// TODO : KAKAO Token 관련 정보 추가
oAuthRepository.save(newOAuth);
-
- Account newAccount = new Account();
- newAccount.setUserId(newUser.getId());
- newAccount.setCash((double) 50_000_000_000L);
- accountRepository.save(newAccount);
+ accountService.createNewAccount(newUser.getId(), CommonValues.INITIAL_USER_CASH);
UserOAuthDetails userOAuthDetails = new UserOAuthDetails(newUser, newOAuth);
diff --git a/src/main/resources/application-h2-mem.yml b/src/main/resources/application-h2-mem.yml
new file mode 100644
index 00000000..d0a6e1fc
--- /dev/null
+++ b/src/main/resources/application-h2-mem.yml
@@ -0,0 +1,4 @@
+spring:
+ jpa:
+ hibernate:
+ ddl-auto: update
diff --git a/src/main/resources/application-it.yml b/src/main/resources/application-it.yml
new file mode 100644
index 00000000..bc275b92
--- /dev/null
+++ b/src/main/resources/application-it.yml
@@ -0,0 +1,4 @@
+spring:
+ jpa:
+ hibernate:
+ ddl-auto: validate
diff --git a/src/main/resources/application-mariadb-local.yml b/src/main/resources/application-mariadb-local.yml
new file mode 100644
index 00000000..fc220807
--- /dev/null
+++ b/src/main/resources/application-mariadb-local.yml
@@ -0,0 +1,9 @@
+spring:
+ jpa:
+ hibernate:
+ ddl-auto: update
+ datasource:
+ url: jdbc:mariadb://mariadb:3306/${MARIADB_DATABASE}
+ driver-class-name: org.mariadb.jdbc.Driver
+ username: ${MARIADB_USER}
+ password: ${MARIADB_PASSWORD}
\ No newline at end of file
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index 275af44a..e240f9e2 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -3,7 +3,7 @@ spring:
cookie:
secure: true
datasource:
- url: jdbc:mariadb://mariadb:3306/${MARIADB_DATABASE}
+ url: jdbc:mariadb://${MARIADB_HOST}:3306/${MARIADB_DATABASE}
driver-class-name: org.mariadb.jdbc.Driver
username: ${MARIADB_USER}
password: ${MARIADB_PASSWORD}
diff --git a/src/main/resources/icons/BTC.svg b/src/main/resources/icons/BTC.svg
new file mode 100644
index 00000000..dbd25db7
--- /dev/null
+++ b/src/main/resources/icons/BTC.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/src/main/resources/icons/DOGE.svg b/src/main/resources/icons/DOGE.svg
new file mode 100644
index 00000000..a5d319cb
--- /dev/null
+++ b/src/main/resources/icons/DOGE.svg
@@ -0,0 +1,21 @@
+
+
+
diff --git a/src/main/resources/icons/ETH.svg b/src/main/resources/icons/ETH.svg
new file mode 100644
index 00000000..bc31816d
--- /dev/null
+++ b/src/main/resources/icons/ETH.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/main/resources/icons/PEPE.svg b/src/main/resources/icons/PEPE.svg
new file mode 100644
index 00000000..fc99221c
--- /dev/null
+++ b/src/main/resources/icons/PEPE.svg
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/main/resources/icons/SOL.svg b/src/main/resources/icons/SOL.svg
new file mode 100644
index 00000000..82d67cdd
--- /dev/null
+++ b/src/main/resources/icons/SOL.svg
@@ -0,0 +1,40 @@
+
+
+
diff --git a/src/main/resources/icons/SUI.svg b/src/main/resources/icons/SUI.svg
new file mode 100644
index 00000000..630d5dae
--- /dev/null
+++ b/src/main/resources/icons/SUI.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/main/resources/icons/TRUMP.svg b/src/main/resources/icons/TRUMP.svg
new file mode 100644
index 00000000..c60a4f93
--- /dev/null
+++ b/src/main/resources/icons/TRUMP.svg
@@ -0,0 +1,43 @@
+
+
+
diff --git a/src/main/resources/icons/USDT.svg b/src/main/resources/icons/USDT.svg
new file mode 100644
index 00000000..17a852e4
--- /dev/null
+++ b/src/main/resources/icons/USDT.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/main/resources/icons/WLD.svg b/src/main/resources/icons/WLD.svg
new file mode 100644
index 00000000..13967bf6
--- /dev/null
+++ b/src/main/resources/icons/WLD.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/main/resources/icons/XRP.svg b/src/main/resources/icons/XRP.svg
new file mode 100644
index 00000000..d0d128ff
--- /dev/null
+++ b/src/main/resources/icons/XRP.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
index 458850e9..acfdc5cc 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -3,11 +3,12 @@
+
- INFO
+ ${LOG_LEVEL}${LOG_PATTERN}
@@ -29,7 +30,7 @@
${LOG_DIR}/was1.%d{yyyy-MM-dd}.%i.log10MB30
- 100MB
+ 50MB
@@ -37,6 +38,11 @@
+
+
+
+
+
@@ -44,6 +50,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/websocket-test.html b/src/main/resources/templates/websocket-test.html
index aa26c865..0f1775a6 100644
--- a/src/main/resources/templates/websocket-test.html
+++ b/src/main/resources/templates/websocket-test.html
@@ -1,361 +1,489 @@
-
+
- 전일 대비 변동률 테스트
-
-
+ 코인 데이터 종합 테스트
-
코인 실시간 데이터 테스트
-
-
-
-
-
-
-
실시간 변동률
-
전일 대비 변동률
+
코인 데이터 종합 테스트
+
+
+
연결 설정
+
+
+
+
+
+
+
+
+
+
중요: SockJS를 사용하지 않고 기본 WebSocket으로 연결합니다.
+
+
+
+
+
+
+
+
상태: 연결 안됨
-
-
-
-
+
+
+
실시간 거래
+
전일 대비
+
OHLC 차트
+
디버그 로그
-
+
실시간 거래 데이터
-
-
구독 후 실시간 데이터가 여기에 표시됩니다.
+
+
+
구독하면 여기에 데이터가 표시됩니다.
+
+
+
+
전일 대비 데이터
+
+
+
구독하면 여기에 데이터가 표시됩니다.
+
+
+
데이터 문제 원인 분석
+
전일 대비 데이터가 모두 0.0으로 표시되는 이유:
+
+
데이터베이스에 어제 및 현재 거래 데이터가 없음
+
RealTimeDataPrevRateService.java의 generatePrevRateData 메서드에서 DB 쿼리 결과가 null