Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d7d7d5e
feat: 호가 단위 정책 작업중
109an94 May 27, 2025
4ff7e11
feat: 호가 단위 정책
109an94 May 28, 2025
28e4826
feat: 주문 강도 조절
109an94 May 28, 2025
05d944d
refactor: 로그 출력 수정
109an94 May 28, 2025
11f331c
Merge remote-tracking branch 'origin/feat/realitybot' into feat/reali…
109an94 May 28, 2025
7e0170b
add: 주석 수정
109an94 May 29, 2025
20502ab
test: 호가 수집 단위테스트
109an94 May 29, 2025
5cd8d40
config: junit realitybot만 실행
109an94 May 30, 2025
cc32af4
refactor: api 요청 중 잘못된 응답 처리
109an94 May 30, 2025
8376686
config: 호가 수집 시각 수정
109an94 May 30, 2025
4d87da4
test: 호가 수집 단위테스트
109an94 May 30, 2025
2d60130
remove: 모듈 결합 전 코드 삭제
109an94 Jun 1, 2025
6879f73
refactor: 코드 개선
109an94 Jun 1, 2025
142717c
test: 단위테스트 코드 작성
109an94 Jun 1, 2025
6ce7966
test: 시세 수집 단위테스트
109an94 Jun 1, 2025
7effaf4
refactor: 코드 개선
109an94 Jun 2, 2025
3276b33
test: 시세 수집 단위테스트
109an94 Jun 2, 2025
e51cb5b
feat: 선형 보간 작업
109an94 Jun 3, 2025
d36a61b
test: 거래소 및 보간 단위 테스트
109an94 Jun 3, 2025
fdd6d6e
feat: 신규 거래소 추가
109an94 Jun 3, 2025
c8aecc1
test: 단위 테스트
109an94 Jun 3, 2025
b504a50
refactor: 가격 정책 변경
109an94 Jun 4, 2025
ebe6e44
test: 단위 테스트
109an94 Jun 4, 2025
04e121f
refactor: exception 수정
109an94 Jun 5, 2025
9742b2c
Merge branch 'dev' of https://github.com/CleanEngine/cleanengine-be i…
109an94 Jun 5, 2025
a3e8a62
refactor: 오류 수정
109an94 Jun 5, 2025
1d9a8ba
refactor: 오류 수정
109an94 Jun 5, 2025
60cabd8
refactor: log 수정
109an94 Jun 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ repositories {
mavenCentral()
}


jacoco { //추가함
toolVersion = "0.8.13"
}
Expand Down
38 changes: 28 additions & 10 deletions src/main/java/com/cleanengine/coin/realitybot/api/ApiScheduler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
import com.cleanengine.coin.common.annotation.WorkingServerProfile;
import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository;
import com.cleanengine.coin.order.domain.Asset;
import com.cleanengine.coin.realitybot.domain.APIVWAPState;
import com.cleanengine.coin.realitybot.dto.Ticks;
import com.cleanengine.coin.realitybot.service.ApiVWAPService;
import com.cleanengine.coin.realitybot.parser.TickParser;
import com.cleanengine.coin.realitybot.service.OrderGenerateService;
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.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
Expand All @@ -29,38 +28,40 @@ public class ApiScheduler {
private final TickServiceManager tickServiceManager;
private final Map<String,Long> lastSequentialIdMap = new ConcurrentHashMap<>();
private final AssetRepository assetRepository;
private final CoinoneAPIClient coinoneAPIClient;
private String ticker;

@Scheduled(fixedRate = 5000)
// @Scheduled(fixedRate = 5000)
public void MarketAllRequest() throws InterruptedException {
List<Asset> tickers = assetRepository.findAll();
for (Asset ticker : tickers){
String tickerName = ticker.getTicker();
MarketDataRequest(tickerName);
Thread.sleep(500);
// Thread.sleep(500);
}
}

public void MarketDataRequest(String ticker){
this.ticker = ticker;
String rawJson = bithumbAPIClient.get(ticker); //api raw데이터
List<Ticks> gson = TickParser.parseGson(rawJson); //json을 list로 변환
// String rawJson = getMarketDataWithFallback(ticker);
List<Ticks> gson = tickParser.parseGson(rawJson); //json을 list로 변환

ApiVWAPService apiVWAPService = tickServiceManager.getService(ticker);
APIVWAPState apiVWAPState = tickServiceManager.getService(ticker);
long lastSeqId = lastSequentialIdMap.getOrDefault(ticker,0L);

//api 중복검사하여 queue에 저장하기
for (int i = gson.size()-1; i >=0 ; i--) {//2차 : 10 - 역순으로 정렬되어 - 순회해야 함.
Ticks ticks = gson.get(i);
if (ticks.getSequential_id() > lastSeqId){ //중복 검증용
apiVWAPService.addTick(ticks);
apiVWAPState.addTick(ticks);
lastSeqId = Math.max(lastSeqId, ticks.getSequential_id()); //중복 id 갱신

}
}
lastSequentialIdMap.put(ticker,lastSeqId);
double vwap = apiVWAPService.getVWAP();
double volume = apiVWAPService.getAvgVolumePerOrder();
double vwap = apiVWAPState.getVWAP();
double volume = apiVWAPState.getAvgVolumePerOrder();
orderGenerateService.generateOrder(ticker,vwap,volume); //1tick 당 매수/매도 3개씩 제작
// log.info("작동확인 {}의 가격 : {} , 볼륨 : {}",ticker, vwap, volume);
}
Expand All @@ -73,6 +74,23 @@ public void destroy() throws Exception { //담긴 Queue데이터 확인용
// orderQueueManagerService.logAllOrders();
// virtualTradeService.printOrderSummary();
}*/
/*public String getMarketDataWithFallback(String ticker) {
try {
// String bithumbJson = bithumbAPIClient.get(ticker);
String bithumbJson = null;

// 예외가 없었어도 비정상 응답일 수 있음 → 예: 빈 JSON 또는 에러 코드
if (bithumbJson == null || bithumbJson.isBlank() || bithumbJson.contains("\"result\":\"error\"")) {
log.warn("Bithumb 응답 비정상, Coinone으로 대체 요청");
return coinoneAPIClient.get(ticker);
}

return bithumbJson;

} catch (Exception e) {
log.error("Bithumb API 오류 발생: {} → Coinone으로 대체 요청", e.getMessage());
return coinoneAPIClient.get(ticker);
}
}*/

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,41 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.*;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.stereotype.Component;
import com.google.gson.Gson;

import java.io.IOException;

@Component
@RequiredArgsConstructor
@Slf4j
public class BithumbAPIClient {
private OkHttpClient client;
private Gson gson;
private final OkHttpClient client;
private String ticker;


public String get(String ticker){ //API를 responseBody에 담아 반환
this.ticker = ticker;
client = new OkHttpClient();
gson = new Gson();
// client = new OkHttpClient();
// gson = new Gson();

// try {
// Thread.sleep(2500);
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }


Request request = new Request.Builder()
.url("https://api.bithumb.com/v1/trades/ticks?market=krw-"+ticker+"&count=10")
.get()
.addHeader("accept", "application/json")
.build();
try (Response response = client.newCall(request).execute()){
if ((response.code() == 400)){
log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker);
}
String responseBody = response.body().string();
// return gson.toJson(response.body().string());
log.debug("{}의 Bithumb API 응답 : {}",ticker,responseBody);
Expand All @@ -37,5 +47,24 @@ public String get(String ticker){ //API를 responseBody에 담아 반환
throw new RuntimeException(e);
}
}
public String getOpeningPrice(String ticker){
this.ticker = ticker;
Request request = new Request.Builder()
.url("https://api.bithumb.com/v1/ticker?markets=KRW-"+ticker)
.get()
.addHeader("accept", "application/json")
.build();
try (Response response = client.newCall(request).execute()){
if ((response.code() == 400)){
log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker);
}
String responseBody = response.body().string();
// return gson.toJson(response.body().string());
log.debug("{}의 OpeningPirce 응답 : {}",ticker,responseBody);
return responseBody;
} catch (IOException e) {
throw new RuntimeException("API 요청 중 예외 발생",e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.cleanengine.coin.realitybot.api;


import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
@Slf4j
public class CoinoneAPIClient {
private final OkHttpClient client;
private String ticker;


public String get(String ticker){ //API를 responseBody에 담아 반환
this.ticker = ticker;
Request request = new Request.Builder()
.url("https://api.coinone.co.kr/public/v2/trades/KRW/"+ticker+"?size=10")
.get()
.addHeader("accept", "application/json")
.build();
try (Response response = client.newCall(request).execute()){
if ((response.code() == 400)){
log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker);
}
String responseBody = response.body().string();
// return gson.toJson(response.body().string());
log.info("{}의 Bithumb API 응답 : {}",ticker,responseBody);
return responseBody;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/* public String getOpeningPrice(String ticker){
this.ticker = ticker;
Request request = new Request.Builder()
.url("https://api.bithumb.com/v1/ticker?markets=KRW-"+ticker)
.get()
.addHeader("accept", "application/json")
.build();
try (Response response = client.newCall(request).execute()){
if ((response.code() == 400)){
log.warn("잘못된 ticker를 입력하였습니다. 입력된 ticker : {}",ticker);
}
String responseBody = response.body().string();
// return gson.toJson(response.body().string());
log.debug("{}의 OpeningPirce 응답 : {}",ticker,responseBody);
return responseBody;
} catch (IOException e) {
throw new RuntimeException("API 요청 중 예외 발생",e);
}
}*/

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.cleanengine.coin.realitybot.api;

import com.cleanengine.coin.order.domain.Asset;
import com.cleanengine.coin.order.infra.AssetRepository;
import com.cleanengine.coin.realitybot.dto.OpeningPrice;
import com.cleanengine.coin.realitybot.parser.OpeningPriceParser;
import com.cleanengine.coin.realitybot.vo.UnitPricePolicy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Service
@RequiredArgsConstructor
public class UnitPriceRefresher implements ApplicationRunner {
private final UnitPricePolicy unitPricePolicy;
private final AssetRepository assetRepository;
private final BithumbAPIClient bithumbAPIClient;
private final OpeningPriceParser openingPriceParser;
private final Map<String ,Double> unitPriceCache = new ConcurrentHashMap<>();

@Override
public void run(ApplicationArguments args){
log.info("Running Unit Price Refresher...");
initializeUnitPrices();
}

public void initializeUnitPrices() {
List<Asset> tickers = assetRepository.findAll();
for (Asset ticker : tickers){
double unitPrice = fetchOpeningPriceFromAPI(ticker.getTicker());
unitPriceCache.put(ticker.getTicker(),unitPrice);
}
}

@Scheduled(cron = "${bot-handler.corn}")
public void refreshUnitPrices() {
initializeUnitPrices();
// List<Asset> tickers = assetRepository.findAll();
// for (Asset ticker : tickers){
// double unitPrice = fetchOpeningPriceFromAPI(ticker.getTicker());
// unitPriceCache.put(ticker.getTicker(),unitPrice);
// }

}

private double fetchOpeningPriceFromAPI(String ticker) {
String rawJson = bithumbAPIClient.getOpeningPrice(ticker); //api raw데이터
OpeningPrice json = openingPriceParser.parseGson(rawJson); //json을 list로 변환
double unitprice = unitPricePolicy.getUnitPrice(json.getOpening_price());
return unitprice;
}

public double getUnitPriceByTicker(String ticker){
return unitPriceCache.get(ticker);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,4 @@ public OkHttpClient okHttpClient() {
// .addInterceptor()
.build();
}

@Bean
public Queue<Ticks> ticksQueue(){ //공통화 시킴
return new LinkedList<>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.cleanengine.coin.realitybot.config;

import com.cleanengine.coin.realitybot.api.ApiScheduler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.time.Duration;

@Configuration
@EnableScheduling
//@RequiredArgsConstructor
public class SchedulerConfig implements SchedulingConfigurer {

//멀티쓰레드 환경 x
// @Autowired
// private TaskScheduler apiScheduler;
private final ApiScheduler apiScheduler;
@Value("${bot-handler.fixed-rate}")
private final Duration fixedRate;

protected SchedulerConfig(ApiScheduler apiScheduler, @Value("${bot-handler.fixed-rate}") Duration fixedRate) {
this.apiScheduler = apiScheduler;
this.fixedRate = fixedRate;
}

@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
// registrar.setScheduler(apiScheduler); //멀티 쓰레드 x
registrar.addFixedRateTask(() -> {
try {
apiScheduler.MarketAllRequest();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, fixedRate);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package com.cleanengine.coin.realitybot.controller;

import com.cleanengine.coin.realitybot.api.BithumbAPIClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ApiController {
private final BithumbAPIClient bithumbAPIClient;
public ApiController(BithumbAPIClient bithumbAPIClient) {
this.bithumbAPIClient = bithumbAPIClient;
}
@GetMapping("/test")
public String getApiData(){
// return bithumbAPIClient.get(ticekr);
return null;

}}
//초기 api 작동 확인용 -> realitybot controller로 전환 필요
// private final BithumbAPIClient bithumbAPIClient;
// public ApiController(BithumbAPIClient bithumbAPIClient) {
// this.bithumbAPIClient = bithumbAPIClient;
// }
// @GetMapping("/test/{tickerName}")
// public String getApiData(@PathVariable String tickerName) {
// System.out.println("tickername 출력"+tickerName);
// return bithumbAPIClient.get(tickerName);
// }
}
Loading
Loading