Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ out/

### 로컬 환경변수 ###
local.properties
/logs
/logs
docker-compose.override.yml

dd-java-agent.jar
4 changes: 1 addition & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,7 @@ dependencies {
}

tasks.named('test') {
useJUnitPlatform{
excludeTags 'testcontainers'
}
useJUnitPlatform()
finalizedBy jacocoTestReport
}

Expand Down
5 changes: 3 additions & 2 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ services:
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"]
command: ["java", "-jar", "coin-0.0.1-SNAPSHOT.jar", "--spring.profiles.active=dev,it,mariadb-local"]
ports:
- "8080:8080"
- "5005:5005"
env_file:
- ./local.properties
environment:
- TZ=Asia/Seoul
- JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
depends_on:
mariadb:
condition: service_healthy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.cleanengine.coin.common.annotation;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Aspect
@Component
public class TransactionLoggingAspect {

private static final String TRANSACTION_ID_KEY = "txId";

@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object logTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
String txId = UUID.randomUUID().toString().substring(0, 8);
MDC.put(TRANSACTION_ID_KEY, txId);

try {
return joinPoint.proceed();
} finally {
MDC.remove(TRANSACTION_ID_KEY);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.cleanengine.coin.order.adapter.out.persistentce.account;

import com.cleanengine.coin.user.domain.Account;
import jakarta.persistence.LockModeType;
import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface OrderAccountRepository extends CrudRepository<Account, Integer> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
// @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")})
Optional<Account> findByUserId(Integer userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.cleanengine.coin.user.domain.Wallet;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.NoResultException;
import jakarta.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
Expand All @@ -23,6 +24,8 @@ public Optional<Wallet> findWalletBy(Integer userId, String ticker) {
Wallet.class);
query.setParameter("userId", userId);
query.setParameter("ticker", ticker);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
// query.setHint("jakarta.persistence.lock.timeout", "3000");

try{
Wallet wallet = query.getSingleResult();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public List<AssetInfo> getAllAssetInfos(){
return assetRepository.findAll().stream().map(AssetInfo::from).toList();
}

public List<String> getAllAssetTickers(){
return assetRepository.findAll().stream().map(Asset::getTicker).toList();
}

public boolean isAssetExist(String ticker){
if(assetCacheRepository.isAssetExists(ticker)) return true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.validation.Validator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
Expand All @@ -22,11 +23,11 @@
@Service
@RequiredArgsConstructor
@Validated
public class OrderService {
public class OrderService {
private final List<CreateOrderStrategy<?, ?>> createOrderStrategies;
private final Validator validator;

@Transactional
@Transactional(isolation = Isolation.READ_COMMITTED)
public OrderInfo<?> createOrder(OrderCommand.CreateOrder createOrder){
validateCreateOrder(createOrder);
CreateOrderStrategy<?, ?> createOrderStrategy = createOrderStrategies.stream()
Expand All @@ -35,14 +36,14 @@ public OrderInfo<?> createOrder(OrderCommand.CreateOrder createOrder){
return createOrderStrategy.processCreatingOrder(createOrder);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public OrderInfo<?> createOrderWithBot(String ticker, Boolean isBuyOrder, Double orderSize, Double price){
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public void createOrderWithBot(String ticker, Boolean isBuyOrder, Double orderSize, Double price){
Integer userId = isBuyOrder? BUY_ORDER_BOT_ID : SELL_ORDER_BOT_ID;

OrderCommand.CreateOrder createOrder = new OrderCommand.CreateOrder(ticker, userId, isBuyOrder,
false, orderSize, price, LocalDateTime.now(), true);

return createOrder(createOrder);
createOrder(createOrder);
}

protected void validateCreateOrder(OrderCommand.CreateOrder createOrder) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.cleanengine.coin.order.application.strategy;

import com.cleanengine.coin.common.error.DomainValidationException;
import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository;
import com.cleanengine.coin.order.adapter.out.persistentce.order.command.BuyOrderRepository;
import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository;
import com.cleanengine.coin.order.application.AssetService;
import com.cleanengine.coin.order.application.dto.OrderInfo;
import com.cleanengine.coin.order.application.port.out.PublishOrderCreatedPort;
Expand All @@ -12,6 +10,8 @@
import com.cleanengine.coin.order.domain.domainservice.CreateBuyOrderDomainService;
import com.cleanengine.coin.order.domain.domainservice.CreateOrderDomainService;
import com.cleanengine.coin.user.domain.Account;
import com.cleanengine.coin.user.info.infra.AccountRepository;
import com.cleanengine.coin.user.info.infra.WalletRepository;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;

Expand Down Expand Up @@ -57,11 +57,11 @@ protected OrderInfo.BuyOrderInfo extractOrderInfo(Order order) {

public BuyOrderStrategy(PublishOrderCreatedPort publishOrderCreatedPort,
AssetService assetService,
OrderWalletRepository orderWalletRepository,
OrderAccountRepository orderAccountRepository,
WalletRepository walletRepository,
AccountRepository accountRepository,
BuyOrderRepository buyOrderRepository,
CreateBuyOrderDomainService createOrderDomainService) {
super(publishOrderCreatedPort, assetService, orderWalletRepository, orderAccountRepository);
super(publishOrderCreatedPort, assetService, walletRepository, accountRepository);
this.buyOrderRepository = buyOrderRepository;
this.createOrderDomainService = createOrderDomainService;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package com.cleanengine.coin.order.application.strategy;

import com.cleanengine.coin.common.error.DomainValidationException;
import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository;
import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository;
import com.cleanengine.coin.order.application.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.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.AllArgsConstructor;
import org.springframework.validation.FieldError;

Expand All @@ -21,14 +19,13 @@
public abstract class CreateOrderStrategy<T extends Order, S extends OrderInfo<?>> {
protected final PublishOrderCreatedPort publishOrderCreatedPort;
protected final AssetService assetService;
protected final OrderWalletRepository walletRepository;
protected final OrderAccountRepository accountRepository;
protected final WalletRepository walletRepository;
protected final AccountRepository accountRepository;

public S processCreatingOrder(OrderCommand.CreateOrder createOrderCommand){
validateTicker(createOrderCommand.ticker());
T order = createOrder(createOrderCommand);
saveOrder(order);
createWalletIfNeeded(order.getUserId(), order.getTicker());
keepHoldings(order);
publishOrderCreatedPort.publish(new OrderCreated(order));
return extractOrderInfo(order);
Expand All @@ -49,21 +46,12 @@ protected void validateTicker(String ticker){
}

protected T createOrder(OrderCommand.CreateOrder createOrderCommand){
T order = createOrderDomainService().createOrder(

return createOrderDomainService().createOrder(
createOrderCommand.ticker(), createOrderCommand.userId(),
createOrderCommand.isBuyOrder(), createOrderCommand.isMarketOrder(),
createOrderCommand.orderSize(), createOrderCommand.price(),
createOrderCommand.createdAt(), createOrderCommand.isBot());

return order;
}

// TODO 책임이 너무 많은
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.cleanengine.coin.order.application.strategy;

import com.cleanengine.coin.common.error.DomainValidationException;
import com.cleanengine.coin.order.adapter.out.persistentce.account.OrderAccountRepository;
import com.cleanengine.coin.order.adapter.out.persistentce.order.command.SellOrderRepository;
import com.cleanengine.coin.order.adapter.out.persistentce.wallet.OrderWalletRepository;
import com.cleanengine.coin.order.application.AssetService;
import com.cleanengine.coin.order.application.dto.OrderInfo;
import com.cleanengine.coin.order.application.port.out.PublishOrderCreatedPort;
Expand All @@ -12,6 +10,8 @@
import com.cleanengine.coin.order.domain.domainservice.CreateOrderDomainService;
import com.cleanengine.coin.order.domain.domainservice.CreateSellOrderDomainService;
import com.cleanengine.coin.user.domain.Wallet;
import com.cleanengine.coin.user.info.infra.AccountRepository;
import com.cleanengine.coin.user.info.infra.WalletRepository;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;

Expand Down Expand Up @@ -39,7 +39,7 @@ protected void keepHoldings(SellOrder order) throws RuntimeException {
Double orderSize = order.getOrderSize();

Wallet wallet = walletRepository
.findWalletBy(userId, ticker)
.findByAccountIdAndTicker(userId, ticker)
.orElseThrow(()->
new DomainValidationException("Wallet not found",
List.of(new FieldError("wallet", "userId", "user might not exist"),
Expand All @@ -62,11 +62,11 @@ protected OrderInfo.SellOrderInfo extractOrderInfo(Order order) {

public SellOrderStrategy(PublishOrderCreatedPort publishOrderCreatedPort,
AssetService assetService,
OrderWalletRepository orderWalletRepository,
OrderAccountRepository orderAccountRepository,
WalletRepository walletRepository,
AccountRepository accountRepository,
SellOrderRepository sellOrderRepository,
CreateSellOrderDomainService createOrderDomainService) {
super(publishOrderCreatedPort, assetService, orderWalletRepository, orderAccountRepository);
super(publishOrderCreatedPort, assetService, walletRepository, accountRepository);
this.sellOrderRepository = sellOrderRepository;
this.createOrderDomainService = createOrderDomainService;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
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.user.domain.Account;
import com.cleanengine.coin.user.domain.Wallet;
import com.cleanengine.coin.user.info.application.AccountService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -18,36 +15,28 @@

import java.text.DecimalFormat;

import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID;
import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID;

@Slf4j
@Service
@Order(5)
@RequiredArgsConstructor
public class 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 OrderPricePolicy orderPricePolicy;
private final DeviationPricePolicy deviationPricePolicy;
private final OrderVolumePolicy orderVolumePolicy;
private final OrderWalletRepository orderWalletRepository;
private final OrderAccountRepository accountExternalRepository;
private final AccountService accountService;

private final VWAPMetricsRecorder recorder;
private String ticker;


public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//기준 주문금액, 주문량 받기 (tick당 계산되어 들어옴)
this.ticker = ticker;

//호가 정책 적용
this.unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker);
//TODO : 거래쌍 시세에 따른 호가 정책 개발 필요
double unitPrice = unitPriceRefresher.getUnitPriceByTicker(ticker);

// //최근 체결 내역 가져오기
// List<Trade> trades = tradeRepository.findTop10ByTickerOrderByTradeTimeDesc(ticker);
Expand All @@ -58,7 +47,7 @@ public void generateOrder(String ticker, double apiVWAP, double avgVolume) {//
//편차 계산 (vwap 기준)
double trendLineRate = (platformVWAP - apiVWAP)/ apiVWAP;
for(int level : orderLevels) { //1주문당 3회 매수매도 처리
OrderPricePolicy.OrderPrice basePrice = orderPricePolicy.calculatePrice(level,platformVWAP,unitPrice,trendLineRate);
OrderPricePolicy.OrderPrice basePrice = orderPricePolicy.calculatePrice(level,platformVWAP, unitPrice,trendLineRate);
DeviationPricePolicy.AdjustPrice adjustPrice = deviationPricePolicy.adjust(
basePrice.sell(), basePrice.buy(), trendLineRate, apiVWAP, unitPrice);

Expand Down Expand Up @@ -103,29 +92,12 @@ private void createOrderWithFallback(String ticker,boolean isBuy, double volume,
} catch (IllegalArgumentException e) {
log.debug("잔량 부족: {}", e.getMessage());
try {
resetBot(ticker);
accountService.resetBot(ticker);
orderService.createOrderWithBot(ticker, isBuy, volume, price);
} catch (Exception e1) {
log.error("주문 재시도 실패", e1);
}
}
}

protected void resetBot(String ticker){
this.ticker = ticker;
Wallet wallet = orderWalletRepository.findWalletBy(SELL_ORDER_BOT_ID,ticker).get();
wallet.setSize(500_000_000.0);
Wallet wallet2 = orderWalletRepository.findWalletBy(BUY_ORDER_BOT_ID,ticker).get();
wallet2.setSize(0.0);
orderWalletRepository.save(wallet);
orderWalletRepository.save(wallet2);

Account account = accountExternalRepository.findByUserId(SELL_ORDER_BOT_ID).get();
account.setCash(0.0);
Account account2 = accountExternalRepository.findByUserId(BUY_ORDER_BOT_ID).get();
account2.setCash(500_000_000.0);
accountExternalRepository.save(account);
accountExternalRepository.save(account2);
}

}
Loading
Loading