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
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "헤더가 비어있습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'",
Expand All @@ -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);
});
}

Expand All @@ -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);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,41 @@
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) {
futuresPositionRepository.delete(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<FuturesPosition> 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);
}
}
Loading