diff --git a/Tradin-Core/src/main/java/com/tradin/core/balance/domain/Balance.java b/Tradin-Core/src/main/java/com/tradin/core/balance/domain/Balance.java index 5076649..2e9d262 100644 --- a/Tradin-Core/src/main/java/com/tradin/core/balance/domain/Balance.java +++ b/Tradin-Core/src/main/java/com/tradin/core/balance/domain/Balance.java @@ -1,8 +1,13 @@ package com.tradin.core.balance.domain; +import static com.tradin.core.common.exception.ExceptionType.INSUFFICIENT_BALANCE_EXCEPTION; +import static com.tradin.core.common.exception.ExceptionType.INVALID_AMOUNT_EXCEPTION; + import com.tradin.core.account.domain.Account; import com.tradin.core.balance.domain.vo.Amount; import com.tradin.core.common.converter.AmountConverter; +import com.tradin.core.common.exception.ExceptionType; +import com.tradin.core.common.exception.TradinException; import com.tradin.core.common.jpa.AuditTime; import com.tradin.core.strategy.domain.CoinType; import jakarta.persistence.Column; @@ -68,6 +73,33 @@ public void updateAmount(Amount amount) { this.amount = amount; } + public void subtractMargin(Amount amount) { + if (amount.isNegative()) { + throw new TradinException(INVALID_AMOUNT_EXCEPTION); + } + + if (this.amount.isLessThan(amount)) { + throw new TradinException(INSUFFICIENT_BALANCE_EXCEPTION); + } + this.amount = this.amount.subtract(amount); + } + + public void addBalance(Amount amount) { + if (amount.isNegative()) { + throw new TradinException(INVALID_AMOUNT_EXCEPTION); + } + this.amount = this.amount.add(amount); + } + + public void settleProfit(Amount margin, Amount profitAmount) { + addBalance(margin); + + if (profitAmount.isNegative()) { + subtractMargin(profitAmount.negate()); + } + addBalance(profitAmount); + } + public Amount getAmount() { return this.amount; } diff --git a/Tradin-Core/src/main/java/com/tradin/core/balance/domain/vo/Amount.java b/Tradin-Core/src/main/java/com/tradin/core/balance/domain/vo/Amount.java index 2e71863..d2c5a0c 100644 --- a/Tradin-Core/src/main/java/com/tradin/core/balance/domain/vo/Amount.java +++ b/Tradin-Core/src/main/java/com/tradin/core/balance/domain/vo/Amount.java @@ -45,6 +45,9 @@ public Amount negate() { public boolean isPositive() { return value.signum() >= 0; } public boolean isNegative() { return value.signum() < 0; } + public boolean isLessThan(Amount other) { + return this.value.compareTo(other.value) < 0; + } @Override public boolean equals(Object o) { diff --git a/Tradin-Core/src/main/java/com/tradin/core/balance/service/BalanceService.java b/Tradin-Core/src/main/java/com/tradin/core/balance/service/BalanceService.java index 73822f2..29712c2 100644 --- a/Tradin-Core/src/main/java/com/tradin/core/balance/service/BalanceService.java +++ b/Tradin-Core/src/main/java/com/tradin/core/balance/service/BalanceService.java @@ -5,6 +5,7 @@ import com.tradin.core.balance.domain.repository.BalanceRepository; import com.tradin.core.balance.domain.vo.Amount; import com.tradin.core.common.annotation.DistributedLock; +import com.tradin.core.futuresOrder.domain.vo.Margin; import com.tradin.core.strategy.domain.CoinType; import com.tradin.core.common.exception.ExceptionType; import com.tradin.core.common.exception.TradinException; @@ -35,6 +36,19 @@ public Balance findByAccountIdAndCoinType(Long accountId, CoinType coinType) { .orElseThrow(() -> new TradinException(ExceptionType.NOT_FOUND_BALANCE_EXCEPTION)); } + public Balance findUsdtBalanceByAccount(Long accountId) { + return balanceRepository.findByAccountIdAndCoinType(accountId, CoinType.USDT) + .orElseThrow(() -> new TradinException(ExceptionType.NOT_FOUND_BALANCE_EXCEPTION)); + } + + public void subtractMargin(Balance balance, Amount amount) { + balance.subtractMargin(amount); + } + + public void settleProfit(Balance balance, Margin margin, Amount profitAmount) { + balance.settleProfit(Amount.of(margin.getValue()), profitAmount); + } + public void updateBalance(Balance balance, Amount amount) { balance.updateAmount(amount); } diff --git a/Tradin-Core/src/main/java/com/tradin/core/common/exception/ExceptionType.java b/Tradin-Core/src/main/java/com/tradin/core/common/exception/ExceptionType.java index 42179d3..22bbe65 100644 --- a/Tradin-Core/src/main/java/com/tradin/core/common/exception/ExceptionType.java +++ b/Tradin-Core/src/main/java/com/tradin/core/common/exception/ExceptionType.java @@ -12,6 +12,8 @@ @Getter public enum ExceptionType { //400 Bad Request + INSUFFICIENT_BALANCE_EXCEPTION(BAD_REQUEST, "잔고가 부족합니다."), + INVALID_AMOUNT_EXCEPTION(BAD_REQUEST, "유효하지 않은 금액입니다."), //401 Unauthorized EMPTY_HEADER_EXCEPTION(UNAUTHORIZED, "헤더가 비어있습니다."), diff --git a/Tradin-Core/src/main/java/com/tradin/core/futuresOrder/service/FuturesOrderFacadeService.java b/Tradin-Core/src/main/java/com/tradin/core/futuresOrder/service/FuturesOrderFacadeService.java index 1205b8d..f1b2a3d 100644 --- a/Tradin-Core/src/main/java/com/tradin/core/futuresOrder/service/FuturesOrderFacadeService.java +++ b/Tradin-Core/src/main/java/com/tradin/core/futuresOrder/service/FuturesOrderFacadeService.java @@ -16,7 +16,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.concurrent.TimeUnit; @Slf4j @Service @@ -25,7 +24,6 @@ public class FuturesOrderFacadeService { private final FuturesOrderService futuresOrderService; private final BalanceService balanceService; private final FuturesPositionService futuresPositionService; - private final PriceCache priceCache; @DistributedLock( key = "'asset-lock:' + #account.id + ':USDT'", @@ -49,16 +47,19 @@ public void autoTrade(Strategy strategy, Account account, Position position) { private void closeExistingPositionIfExists(Strategy strategy, Account account) { futuresPositionService.findOpenFuturesPositionByAccountAndCoinType(account.getId(), strategy.getCoinType()) .ifPresent(futuresPosition -> { - Price currentPrice = getCurrentPrice(strategy.getCoinType()); // 1. 역방향 주문 생성 - futuresOrderService.orderReversePosition(strategy, account, futuresPosition, currentPrice); + futuresOrderService.orderReversePosition(strategy, account, futuresPosition); // 2. 포지션 정리 futuresPositionService.closePosition(futuresPosition); - // 3. 수익 계산 및 잔고 업데이트 - updateBalanceWithProfit(futuresPosition, currentPrice, account); + // 3. 수익 계산 + Amount profitAmount = futuresPositionService.calculateProfitAmount(futuresPosition); + + // 4. 수익 정산 + Balance balance = balanceService.findUsdtBalanceByAccount(account.getId()); + balanceService.settleProfit(balance, futuresPosition.getMargin(), profitAmount); }); } @@ -67,55 +68,15 @@ private void closeExistingPositionIfExists(Strategy strategy, Account account) { */ private void createNewPosition(Strategy strategy, Account account, Position position) { // 1. 잔고 확인 및 차감 - Balance balance = getBalance(account); - Amount orderAmount = getOrderAmount(balance); - updateBalanceWithMargin(balance, orderAmount); - - // 2. 현재 가격 조회 - Price currentPrice = getCurrentPrice(strategy.getCoinType()); - - // 3. 주문 생성 - futuresOrderService.orderPosition(position.getTradingType(), strategy, account, orderAmount, currentPrice); - - // 4. 포지션 생성 - futuresPositionService.openPosition(strategy.getCoinType(), position.getTradingType(), orderAmount, currentPrice, account); - } - - /** - * 수익 계산 및 잔고 업데이트 - */ - private void updateBalanceWithProfit(FuturesPosition futuresPosition, Price currentPrice, Account account) { - Amount profitAmount = futuresPosition.calculateProfitAmount(currentPrice); - Balance balance = balanceService.findByAccountIdAndCoinType(account.getId(), CoinType.USDT); - balanceService.updateBalance(balance, profitAmount); - } - - /** - * 잔고 조회 - */ - private Balance getBalance(Account account) { - return balanceService.findByAccountIdAndCoinType(account.getId(), CoinType.USDT); - } + Balance balance = balanceService.findUsdtBalanceByAccount(account.getId()); + Amount margin = balanceService.getUsdtAmount(balance); + balanceService.subtractMargin(balance, margin); - /** - * 주문 금액 조회 - */ - private Amount getOrderAmount(Balance balance) { - return Amount.of(balanceService.getUsdtAmount(balance).getValue()); - } + // 2. 주문 생성 + futuresOrderService.orderPosition(position.getTradingType(), strategy, account, margin); - /** - * 잔고에서 마진 차감 - */ - private void updateBalanceWithMargin(Balance balance, Amount orderAmount) { - balanceService.updateBalance(balance, orderAmount.negate()); - } - - /** - * 현재 가격 조회 - */ - private Price getCurrentPrice(CoinType coinType) { - return priceCache.getPrice(coinType); + // 3. 포지션 생성 + futuresPositionService.openPosition(strategy.getCoinType(), position.getTradingType(), margin, account); } diff --git a/Tradin-Core/src/main/java/com/tradin/core/futuresOrder/service/FuturesOrderService.java b/Tradin-Core/src/main/java/com/tradin/core/futuresOrder/service/FuturesOrderService.java index c274213..0c73db9 100644 --- a/Tradin-Core/src/main/java/com/tradin/core/futuresOrder/service/FuturesOrderService.java +++ b/Tradin-Core/src/main/java/com/tradin/core/futuresOrder/service/FuturesOrderService.java @@ -5,6 +5,7 @@ import com.tradin.core.futuresOrder.domain.FuturesOrder; import com.tradin.core.futuresOrder.domain.OrderStatus; import com.tradin.core.futuresOrder.domain.repository.FuturesOrderRepository; +import com.tradin.core.price.domain.PriceCache; import com.tradin.core.price.domain.vo.Price; import com.tradin.core.strategy.domain.Strategy; import com.tradin.core.strategy.domain.TradingType; @@ -15,15 +16,17 @@ @Service @RequiredArgsConstructor public class FuturesOrderService { + private final PriceCache priceCache; + private final FuturesOrderRepository futuresOrderRepository; - public FuturesOrder orderPosition(TradingType tradingType, Strategy strategy, Account account, Amount amount, Price currentPrice) { - FuturesOrder futuresOrder = FuturesOrder.of(tradingType, currentPrice, amount, OrderStatus.FILLED, account, strategy); - return futuresOrderRepository.save(futuresOrder); + public void orderPosition(TradingType tradingType, Strategy strategy, Account account, Amount amount) { + FuturesOrder futuresOrder = FuturesOrder.of(tradingType, priceCache.getPrice(strategy.getCoinType()), amount, OrderStatus.FILLED, account, strategy); + futuresOrderRepository.save(futuresOrder); } - public FuturesOrder orderReversePosition(Strategy strategy, Account account, FuturesPosition futuresPosition, Price currentPrice) { + public void orderReversePosition(Strategy strategy, Account account, FuturesPosition futuresPosition) { TradingType reverseTradingType = futuresPosition.isPositionLong() ? TradingType.SHORT : TradingType.LONG; - return orderPosition(reverseTradingType, strategy, account, Amount.of(futuresPosition.getAmount().getValue()), currentPrice); + orderPosition(reverseTradingType, strategy, account, Amount.of(futuresPosition.getAmount().getValue())); } } diff --git a/Tradin-Core/src/main/java/com/tradin/core/futuresPosition/domain/FuturesPosition.java b/Tradin-Core/src/main/java/com/tradin/core/futuresPosition/domain/FuturesPosition.java index 03c23b4..3185413 100644 --- a/Tradin-Core/src/main/java/com/tradin/core/futuresPosition/domain/FuturesPosition.java +++ b/Tradin-Core/src/main/java/com/tradin/core/futuresPosition/domain/FuturesPosition.java @@ -118,18 +118,14 @@ public boolean isPositionShort() { return this.tradingType.isShort(); } - public Amount calculateProfitAmount(Price currentPrice) { - BigDecimal entryPriceValue = this.entryPrice.getValue(); - BigDecimal currentPriceValue = currentPrice.getValue(); - - if (this.tradingType.isLong()) { - // 롱 포지션: (현재가 - 진입가) * 수량 - BigDecimal profitPerUnit = currentPriceValue.subtract(entryPriceValue); - return Amount.of(profitPerUnit.multiply(this.amount.getValue())); - } else { - // 숏 포지션: (진입가 - 현재가) * 수량 - BigDecimal profitPerUnit = entryPriceValue.subtract(currentPriceValue); - return Amount.of(profitPerUnit.multiply(this.amount.getValue())); - } + public Amount calculateProfitAmount(Price price) { + BigDecimal currentPrice = price.getValue(); + BigDecimal entryPrice = this.entryPrice.getValue(); + + BigDecimal profit = this.tradingType.isLong() + ? currentPrice.subtract(entryPrice).multiply(amount.getValue()).divide(entryPrice, 2, RoundingMode.DOWN) + : entryPrice.subtract(currentPrice).multiply(amount.getValue()).divide(entryPrice, 2, RoundingMode.DOWN); + + return Amount.of(profit); } } diff --git a/Tradin-Core/src/main/java/com/tradin/core/futuresPosition/service/FuturesPositionService.java b/Tradin-Core/src/main/java/com/tradin/core/futuresPosition/service/FuturesPositionService.java index 180b844..bc9440d 100644 --- a/Tradin-Core/src/main/java/com/tradin/core/futuresPosition/service/FuturesPositionService.java +++ b/Tradin-Core/src/main/java/com/tradin/core/futuresPosition/service/FuturesPositionService.java @@ -4,16 +4,23 @@ import com.tradin.core.balance.domain.vo.Amount; import com.tradin.core.futuresPosition.domain.FuturesPosition; import com.tradin.core.futuresPosition.domain.repository.FuturesPositionRepository; +import com.tradin.core.price.domain.PriceCache; import com.tradin.core.strategy.domain.CoinType; import com.tradin.core.strategy.domain.TradingType; import com.tradin.core.price.domain.vo.Price; +import java.math.BigDecimal; +import java.math.RoundingMode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.Optional; +@Slf4j @Service @RequiredArgsConstructor public class FuturesPositionService { + private final PriceCache priceCache; + private final FuturesPositionRepository futuresPositionRepository; public void closePosition(FuturesPosition futuresPosition) { @@ -21,12 +28,17 @@ public void closePosition(FuturesPosition futuresPosition) { futuresPositionRepository.flush(); } - public FuturesPosition openPosition(CoinType coinType, TradingType tradingType, Amount amount, Price price, Account account) { - FuturesPosition futuresPosition = FuturesPosition.of(coinType, tradingType, price, amount, account); + public FuturesPosition openPosition(CoinType coinType, TradingType tradingType, Amount amount, Account account) { + FuturesPosition futuresPosition = FuturesPosition.of(coinType, tradingType, priceCache.getPrice(coinType), amount, account); return futuresPositionRepository.save(futuresPosition); } public Optional findOpenFuturesPositionByAccountAndCoinType(Long accountId, CoinType coinType) { return futuresPositionRepository.findOpenFuturesPositionByAccountAndCoinType(accountId, coinType); } + + public Amount calculateProfitAmount(FuturesPosition futuresPosition) { + Price currentPrice = priceCache.getPrice(futuresPosition.getCoinType()); + return futuresPosition.calculateProfitAmount(currentPrice); + } }