Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ dependencies {

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

implementation 'io.micrometer:micrometer-registry-prometheus'

implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.google.code.gson:gson:2.13.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
import com.cleanengine.coin.order.adapter.out.persistentce.asset.AssetRepository;
import com.cleanengine.coin.order.domain.Asset;
import com.cleanengine.coin.realitybot.domain.APIVWAPState;
import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder;
import com.cleanengine.coin.realitybot.dto.Ticks;
import com.cleanengine.coin.realitybot.parser.TickParser;
import com.cleanengine.coin.realitybot.service.OrderGenerateService;
import com.cleanengine.coin.realitybot.service.TickServiceManager;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
Expand All @@ -29,16 +33,20 @@ public class ApiScheduler {
private final Map<String,Long> lastSequentialIdMap = new ConcurrentHashMap<>();
private final AssetRepository assetRepository;
private final CoinoneAPIClient coinoneAPIClient;
private final VWAPMetricsRecorder recorder;
private final MeterRegistry meterRegistry;
private String ticker;

// @Scheduled(fixedRate = 5000)
@Scheduled(fixedRate = 5000)
public void MarketAllRequest() throws InterruptedException {
List<Asset> tickers = assetRepository.findAll();
for (Asset ticker : tickers){
String tickerName = ticker.getTicker();
MarketDataRequest(tickerName);
// Thread.sleep(500);
}
Timer timer = meterRegistry.timer("apischeduler.request.duration");
timer.record(() -> {
List<Asset> tickers = assetRepository.findAll();
for (Asset ticker : tickers) {
String tickerName = ticker.getTicker();
MarketDataRequest(tickerName);
}
});
}

public void MarketDataRequest(String ticker){
Expand All @@ -62,7 +70,10 @@ public void MarketDataRequest(String ticker){
lastSequentialIdMap.put(ticker,lastSeqId);
double vwap = apiVWAPState.getVWAP();
double volume = apiVWAPState.getAvgVolumePerOrder();
recorder.recordApiVwap(ticker,vwap);

orderGenerateService.generateOrder(ticker,vwap,volume); //1tick 당 매수/매도 3개씩 제작

// log.info("작동확인 {}의 가격 : {} , 볼륨 : {}",ticker, vwap, volume);
}

Expand All @@ -89,7 +100,7 @@ public void destroy() throws Exception { //담긴 Queue데이터 확인용

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;

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

@Slf4j
@Service
@Component
@RequiredArgsConstructor
public class UnitPriceRefresher implements ApplicationRunner {
private final UnitPricePolicy unitPricePolicy;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.cleanengine.coin.realitybot.config;

import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

//@Profile("actuator")
@Configuration
public class MetricConfig {

@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "my-app");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ public void addTick(Ticks tick){
if (ticksQueue.size() >= maxQueueSize) {
//10개 이상이 되면 선착순으로 제거해나감
Ticks removed = ticksQueue.poll();
if (removed != null){
calculator.removeTrade(removed.getTrade_price(), removed.getTrade_volume());
}
}
}
//초기엔 들어온 갯수에 따라 증가시켜서 계산함
ticksQueue.add(tick);
calculator.recordTrade(tick.getTrade_price(),tick.getTrade_volume());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.cleanengine.coin.realitybot.domain;

import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

@Component
@RequiredArgsConstructor
public class VWAPMetricsRecorder {
private final MeterRegistry meterRegistry;
private final ConcurrentHashMap<String, AtomicReference<Double>> orderPriceMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, DistributionSummary> orderPriceSummery = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicReference<Double>> apiVwapMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicReference<Double>> platformVwapMap = new ConcurrentHashMap<>();

public void recordPrice(String ticker, boolean isBuy, double price){
String type = isBuy ? "buy" : "sell";
String timeStamp = Instant.now().toString();
// String key = ticker +"|"+type+"|"+timeStamp;
String key = ticker +"|"+type;


AtomicReference<Double> priceRef = orderPriceMap.computeIfAbsent(key, k -> {

AtomicReference<Double> value = new AtomicReference<>(price);

meterRegistry.gauge("order_price",
Tags.of("ticker",ticker,"type",type)
,value,AtomicReference::get);
return value;
});
priceRef.set(price);

DistributionSummary summary = orderPriceSummery.computeIfAbsent(key,k ->
DistributionSummary.builder("order_price_summary")
.tags(Tags.of("ticker",ticker,"type",type))
.publishPercentiles(0.05,0.95)
.register(meterRegistry)
);
summary.record(price);
}


public void recordApiVwap(String ticker, double price){
apiVwapMap.computeIfAbsent(ticker, t -> {
AtomicReference<Double> ref = new AtomicReference<>(price);
meterRegistry.gauge("api_vwap",Tags.of("ticker",t),ref,AtomicReference::get);
return ref;
}).set(price);
}
public void recordPlatformVwap(String ticker, double price){
platformVwapMap.computeIfAbsent(ticker, t -> {
AtomicReference<Double> ref = new AtomicReference<>(price);
meterRegistry.gauge("platform_vwap",Tags.of("ticker",t),ref,AtomicReference::get);
return ref;
}).set(price);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class Ticks {
private String trade_date_utc; // LocalDate
private String trade_time_utc; // LocalTime
private String timestamp; //instant 에러 발생
private float trade_price;
private double trade_price;
private double trade_volume;
private float prev_closing_price;
private double change_price;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.cleanengine.coin.realitybot.service;

import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository;
import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository;
import com.cleanengine.coin.order.application.OrderService;
import com.cleanengine.coin.realitybot.api.UnitPriceRefresher;
import com.cleanengine.coin.realitybot.domain.VWAPMetricsRecorder;
import com.cleanengine.coin.realitybot.vo.DeviationPricePolicy;
import com.cleanengine.coin.realitybot.vo.OrderPricePolicy;
import com.cleanengine.coin.realitybot.vo.OrderVolumePolicy;
import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository;
import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository;
import com.cleanengine.coin.trade.entity.Trade;
import com.cleanengine.coin.trade.repository.TradeRepository;
import com.cleanengine.coin.user.domain.Account;
import com.cleanengine.coin.user.domain.Wallet;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,8 +17,6 @@
import org.springframework.stereotype.Service;

import java.text.DecimalFormat;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID;
import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID;
Expand All @@ -29,19 +26,20 @@
@Order(5)
@RequiredArgsConstructor
public class OrderGenerateService {
private final VWAPMetricsRecorder VWAPMetricsRecorder;
@Value("${bot-handler.order-level}")
private int[] orderLevels; //체결 강도
private double unitPrice = 0; //TODO : 거래쌍 시세에 따른 호가 정책 개발 필요
private final UnitPriceRefresher unitPriceRefresher;
private final PlatformVWAPService platformVWAPService;
private final OrderService orderService;
private final TradeRepository tradeRepository;
private final VWAPerrorInJectionScheduler vwaPerrorInJectionScheduler;
private final OrderPricePolicy orderPricePolicy;
private final DeviationPricePolicy deviationPricePolicy;
private final OrderVolumePolicy orderVolumePolicy;
private final OrderWalletRepository orderWalletRepository;
private final OrderAccountRepository accountExternalRepository;

private final VWAPMetricsRecorder recorder;
private String ticker;


Expand All @@ -51,15 +49,14 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//
//호가 정책 적용
this.unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker);

//최근 체결 내역 가져오기
List<Trade> trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker);
// //최근 체결 내역 가져오기
// List<Trade> trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker);

// Platform 기반 가격 생성 (10개 이하, 10개 이상에 따른 가격 생성)
double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,trades,apiVWAP);

double platformVWAP = platformVWAPService.calculateVWAPbyTrades(ticker,apiVWAP);
recorder.recordPlatformVwap(ticker,platformVWAP);
//편차 계산 (vwap 기준)
double trendLineRate = (platformVWAP - apiVWAP)/ apiVWAP;

for(int level : orderLevels) { //1주문당 3회 매수매도 처리
OrderPricePolicy.OrderPrice basePrice = orderPricePolicy.calculatePrice(level,platformVWAP,unitPrice,trendLineRate);
DeviationPricePolicy.AdjustPrice adjustPrice = deviationPricePolicy.adjust(
Expand All @@ -74,15 +71,7 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//
createOrderWithFallback(ticker,false, sellVolume, sellPrice);
createOrderWithFallback(ticker,true, buyVolume, buyPrice);


try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// vwaPerrorInJectionScheduler.enableInjection(); //에러 발생기 비활성화

/* DecimalFormat df = new DecimalFormat("#,##0.00");
/* DecimalFormat df = new DecimalFormat("#,##0.00");
DecimalFormat dfv = new DecimalFormat("#,###.########");
//모니터링용
System.out.println("sellPrice = " + df.format(sellPrice));
Expand All @@ -92,8 +81,7 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//
System.out.println("buyVolume = " + dfv.format(buyVolume));

System.out.println("====================================");
System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP));
*/
System.out.println(ticker+"의 현재 시장 vwap :"+df.format(apiVWAP)+" | 현재 플랫폼 vwap :"+df.format(platformVWAP));*/
}
/*System.out.println("📦"+ticker+" [체결 기록 Top 10]");
trades.forEach(t ->
Expand All @@ -109,7 +97,7 @@ private void createOrderWithFallback(String ticker,boolean isBuy, double volume,
new DecimalFormat("#,###.########").format(volume));
return;
}

recorder.recordPrice(ticker,isBuy,price);
try {
orderService.createOrderWithBot(ticker, isBuy, volume, price);
} catch (IllegalArgumentException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,65 @@

import com.cleanengine.coin.realitybot.domain.PlatformVWAPState;
import com.cleanengine.coin.trade.entity.Trade;
import lombok.extern.slf4j.Slf4j;
import com.cleanengine.coin.trade.repository.TradeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

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

@Service
@Slf4j
@RequiredArgsConstructor
public class PlatformVWAPService {//TODO 가상 시장 조회용 사라질 예정임
private final TradeRepository tradeRepository;

Map<String, PlatformVWAPState> vwapMap = new ConcurrentHashMap<>();
Map<String, LocalDateTime> lastTradeTimeMap = new ConcurrentHashMap<>();


public double calculateVWAPbyTrades(String ticker,List<Trade> trades,double apiVWAP) {
public double calculateVWAPbyTrades(String ticker,double apiVWAP) {
PlatformVWAPState state = vwapMap.computeIfAbsent(ticker, PlatformVWAPState::new);
if (trades.size() < 10){
//체결 내역이 10개 이하일 경우 자체 계산
return generateVWAP(apiVWAP);
LocalDateTime lastTradeTime = lastTradeTimeMap.get(ticker);

//최근 체결 내역 가져오기
List<Trade> trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker);

if ( trades.size() < 10){
//체결 내역이 10개 이하일 경우 자체 계산
return generateVWAP(apiVWAP);
}
LocalDateTime newestTime = trades.get(0).getTradeTime();
if (lastTradeTime == null) {
lastTradeTimeMap.put(ticker, newestTime);
state.addTrades(trades);
return state.getVWAP();
}
boolean containsSameTime = false;
for (Trade trade : trades) {
if (trade.getTradeTime().isEqual(lastTradeTime)) {
containsSameTime = true;
break;
}
}

if (!containsSameTime) {
trades = tradeRepository.findByTickerAndTradeTimeGreaterThanEqualOrderByTradeTimeDesc(ticker, lastTradeTime);
newestTime = trades.get(0).getTradeTime();
lastTradeTimeMap.put(ticker, newestTime);
}

//=================
state.addTrades(trades);

/*System.out.println("📦"+ticker+" [체결 기록]");
state.addTrades(trades);trades.forEach(t ->
System.out.printf("🕒 %s | 가격: %.0f | 수량: %.8f | 매수: #%d ↔ 매도: #%d%n",
t.getTradeTime(), t.getPrice(), t.getSize(), t.getBuyUserId(), t.getSellUserId())
);*/


return state.getVWAP();
}

Expand Down
Loading
Loading