Skip to content

Commit

Permalink
Merge pull request #77 from CSID-DGU/feature/#33/trading
Browse files Browse the repository at this point in the history
[feat] : 자동매매 기능 구현
  • Loading branch information
bbbang105 authored Jun 17, 2024
2 parents 67936bf + bfe661f commit 73b10d9
Show file tree
Hide file tree
Showing 16 changed files with 429 additions and 149 deletions.
13 changes: 13 additions & 0 deletions backend/src/main/java/org/dgu/backend/common/BaseEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;
import java.time.ZoneId;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public abstract class BaseEntity {

@CreatedDate
@Column(name = "created_at", updatable = false)
@Convert(converter = Jsr310JpaConverters.LocalDateTimeConverter.class)
Expand All @@ -22,4 +24,15 @@ public abstract class BaseEntity {
@Column(name = "updated_at")
@Convert(converter = Jsr310JpaConverters.LocalDateTimeConverter.class)
private LocalDateTime updatedAt;

@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")).plusHours(9);
this.updatedAt = this.createdAt;
}

@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")).plusHours(9);
}
}
22 changes: 19 additions & 3 deletions backend/src/main/java/org/dgu/backend/domain/TradingOption.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class TradingOption extends BaseEntity {
@Column(name = "trading_unit_price", nullable = false)
private Long tradingUnitPrice;

@Column(name = "coin_count", nullable = false, scale = 10)
@Column(name = "coin_count", nullable = false, precision = 30, scale = 15)
private BigDecimal coinCount;

@Column(name = "trading_count", nullable = false)
Expand All @@ -56,24 +56,40 @@ public class TradingOption extends BaseEntity {
private LocalDateTime endDate;

@Builder
public TradingOption(User user, Portfolio portfolio, Long initialCapital, Long currentCapital, Long tradingUnitPrice, int tradingCount, int buyingCount, LocalDateTime startDate, LocalDateTime endDate) {
public TradingOption(User user, Portfolio portfolio, Long initialCapital, Long currentCapital, Long tradingUnitPrice, int tradingCount, LocalDateTime startDate, LocalDateTime endDate) {
this.user = user;
this.portfolio = portfolio;
this.initialCapital = initialCapital;
this.currentCapital = currentCapital;
this.tradingUnitPrice = tradingUnitPrice;
this.coinCount = BigDecimal.ZERO;
this.tradingCount = tradingCount;
this.buyingCount = buyingCount;
this.buyingCount = 0;
this.startDate = startDate;
this.endDate = endDate;
}

public void updateCurrentCapital(Long currentCapital) {
this.currentCapital = currentCapital;
}

public void updateInitialCapital(Long initialCapital) {
this.initialCapital = initialCapital;
}

public void updateAvgPrice(Double avgPrice) {
this.avgPrice = avgPrice;
}

public void updateCoinCount(BigDecimal coinCount) {
this.coinCount = coinCount;
}

public void resetBuyingCount() {
this.buyingCount= 0;
}

public void plusBuyingCount() {
this.buyingCount++;
}
}
3 changes: 3 additions & 0 deletions backend/src/main/java/org/dgu/backend/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public class User extends BaseEntity {
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private TradingOption tradingOption;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserTradingLog> userTradingLogs;

@Builder
public User(UUID userId, String name, String provider, String providerId) {
this.userId = userId;
Expand Down
50 changes: 50 additions & 0 deletions backend/src/main/java/org/dgu/backend/domain/UserTradingLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.dgu.backend.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.dgu.backend.common.BaseEntity;

import java.math.BigDecimal;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "users_trading_logs")
public class UserTradingLog extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "trading_logs_id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "users_id", foreignKey = @ForeignKey(name = "users_trading_logs_fk_portfolios_id"))
private User user;

@Column(name = "type", nullable = false)
private String type;

@Column(name = "capital", nullable = false)
private Long capital;

@Column(name = "coin", nullable = false, precision = 30, scale = 15)
private BigDecimal coin;

@Column(name = "coin_price", nullable = false)
private Double coinPrice;

@Column(name = "rate", nullable = false)
private Double rate;

@Builder
public UserTradingLog(User user, String type, Long capital, BigDecimal coin, Double coinPrice, Double rate){
this.user = user;
this.type = type;
this.capital = capital;
this.coin = coin;
this.coinPrice = coinPrice;
this.rate = rate;
}
}
1 change: 0 additions & 1 deletion backend/src/main/java/org/dgu/backend/dto/TradingDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public TradingOption to(User user, Portfolio portfolio, PortfolioOption portfoli
.currentCapital(fund)
.tradingUnitPrice(fund / portfolioOption.getTradingUnit())
.tradingCount(portfolioOption.getTradingUnit())
.buyingCount(0)
.startDate(startDate)
.endDate(endDate)
.build();
Expand Down
6 changes: 4 additions & 2 deletions backend/src/main/java/org/dgu/backend/dto/UpbitDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import org.dgu.backend.domain.Market;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public class UpbitDto {
@Builder
Expand Down Expand Up @@ -137,12 +136,13 @@ public static OrderRequest of(String market, String side, BigDecimal volume, Dou
public static class OrderResponse {
private String uuid;
private String side;
@JsonProperty("ord_type")
private String ordType;
private String price;
private String state;
private String market;
@JsonProperty("created_at")
private LocalDateTime createdAt;
private String createdAt;
private String volume;
@JsonProperty("remaining_volume")
private String remainingVolume;
Expand All @@ -157,5 +157,7 @@ public static class OrderResponse {
private String executedVolume;
@JsonProperty("trades_count")
private Integer tradesCount;
@JsonProperty("time_in_force")
private Integer timeInForce;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,27 @@

@Getter
@RequiredArgsConstructor
public enum UpbitErrorResult implements BaseErrorCode {
public enum UpbitErrorResult implements BaseErrorCode {
// HTTP 401 Unauthorized 에러 처리
INVALID_QUERY_PAYLOAD(HttpStatus.UNAUTHORIZED, "invalid_query_payload", "JWT 헤더의 페이로드가 올바르지 않습니다. 서명에 사용한 페이로드 값을 확인해주세요."),
JWT_VERIFICATION(HttpStatus.UNAUTHORIZED, "jwt_verification", "JWT 헤더 검증에 실패했습니다. 토큰이 올바르게 생성, 서명되었는지 확인해주세요."),
EXPIRED_ACCESS_KEY(HttpStatus.UNAUTHORIZED, "expired_access_key", "API 키가 만료되었습니다."),
NONCE_USED(HttpStatus.UNAUTHORIZED, "nonce_used", "이미 요청한 nonce값이 다시 사용되었습니다. JWT 헤더 페이로드의 nonce 값은 매번 새로운 값을 사용해야합니다."),
NO_AUTHORIZATION_IP(HttpStatus.UNAUTHORIZED, "no_authorization_ip", "허용되지 않은 IP 주소입니다."),
OUT_OF_SCOPE(HttpStatus.UNAUTHORIZED, "out_of_scope", "허용되지 않은 기능입니다."),
FAIL_GET_RESPONSE(HttpStatus.UNAUTHORIZED, "401", "업비트에서 데이터를 가져오는 데 실패했습니다."),
UNAUTHORIZED_IP(HttpStatus.UNAUTHORIZED, "401", "허용되지 않은 IP 주소입니다."),
UNAUTHORIZED_KEY(HttpStatus.UNAUTHORIZED, "401", "올바른 업비트 키가 아닙니다.");
UNAUTHORIZED_KEY(HttpStatus.UNAUTHORIZED, "401", "올바른 업비트 키가 아닙니다."),
// HTTP 400 Bad Request 에러 처리
CREATE_ASK_ERROR(HttpStatus.BAD_REQUEST, "create_ask_error", "주문 요청 정보가 올바르지 않습니다."),
CREATE_BID_ERROR(HttpStatus.BAD_REQUEST, "create_bid_error", "주문 요청 정보가 올바르지 않습니다."),
INSUFFICIENT_FUNDS_ASK(HttpStatus.BAD_REQUEST, "insufficient_funds_ask", "매수 가능 잔고가 부족합니다."),
INSUFFICIENT_FUNDS_BID(HttpStatus.BAD_REQUEST, "insufficient_funds_bid", "매도 가능 잔고가 부족합니다."),
UNDER_MIN_TOTAL_ASK(HttpStatus.BAD_REQUEST, "under_min_total_ask", "주문 요청 금액이 최소주문금액 미만입니다."),
UNDER_MIN_TOTAL_BID(HttpStatus.BAD_REQUEST, "under_min_total_bid", "주문 요청 금액이 최소주문금액 미만입니다."),
WITHDRAW_ADDRESS_NOT_REGISTERED(HttpStatus.BAD_REQUEST, "withdraw_address_not_registerd", "허용되지 않은 출금 주소입니다. 허용 목록에 등록된 주소로만 출금이 가능합니다."),
VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "validation_error", "잘못된 API 요청입니다. 누락된 파라미터가 없는지 확인해주세요."),
// 그 외
HAS_NO_BITCOIN(HttpStatus.NOT_FOUND, "404", "비트코인을 소유하고 있지 않습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.dgu.backend.repository;

import io.lettuce.core.dynamic.annotation.Param;
import org.dgu.backend.domain.User;
import org.dgu.backend.domain.UserTradingLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface UserTradingLogRepository extends JpaRepository<UserTradingLog,Long> {
@Query("SELECT utl FROM UserTradingLog utl WHERE utl.user = :user " +
"AND utl.createdAt > (SELECT MAX(utl2.createdAt) FROM UserTradingLog utl2 WHERE utl2.user = :user AND utl2.type = 'SELL')")
List<UserTradingLog> findRecentLogsAfterLastSell(@Param("user") User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
import lombok.extern.slf4j.Slf4j;
import org.dgu.backend.constant.Coin;
import org.dgu.backend.domain.Market;
import org.dgu.backend.domain.UpbitKey;
import org.dgu.backend.domain.User;
import org.dgu.backend.domain.UserCoin;
import org.dgu.backend.dto.DashBoardDto;
import org.dgu.backend.dto.UpbitDto;
import org.dgu.backend.exception.*;
import org.dgu.backend.exception.MarketErrorResult;
import org.dgu.backend.exception.MarketException;
import org.dgu.backend.repository.MarketRepository;
import org.dgu.backend.repository.UpbitKeyRepository;
import org.dgu.backend.repository.UserCoinRepository;
import org.dgu.backend.util.JwtUtil;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -29,21 +28,18 @@
@RequiredArgsConstructor
@Slf4j
public class DashBoardServiceImpl implements DashBoardService {
@Value("${upbit.url.account}")
private String UPBIT_URL_ACCOUNT;
@Value("${upbit.url.ticker}")
private String UPBIT_URL_TICKER;
private final JwtUtil jwtUtil;
private final UpbitApiClient upbitApiClient;
private final UpbitKeyRepository upbitKeyRepository;
private final UserCoinRepository userCoinRepository;
private final MarketRepository marketRepository;

// 유저 업비트 잔고를 반환하는 메서드
@Override
public DashBoardDto.UserAccountResponse getUserAccount(String authorizationHeader) {
User user = jwtUtil.getUserFromHeader(authorizationHeader);
UpbitDto.Account[] accounts = getUpbitAccounts(user);
UpbitDto.Account[] accounts = upbitApiClient.getUpbitAccounts(user);

BigDecimal accountSum = getAccountSum(accounts);

Expand All @@ -56,7 +52,7 @@ public DashBoardDto.UserAccountResponse getUserAccount(String authorizationHeade
@Override
public List<DashBoardDto.UserCoinResponse> getUserCoins(String authorizationHeader) {
User user = jwtUtil.getUserFromHeader(authorizationHeader);
UpbitDto.Account[] accounts = getUpbitAccounts(user);
UpbitDto.Account[] accounts = upbitApiClient.getUpbitAccounts(user);

return processUserCoins(accounts, user);
}
Expand Down Expand Up @@ -159,15 +155,4 @@ private BigDecimal getCoinPriceIncreaseRate(UserCoin userCoin, BigDecimal curPri
.divide(pastValue, 6, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}

// 유저 업비트 계좌 정보를 조회하는 메서드
private UpbitDto.Account[] getUpbitAccounts(User user) {
UpbitKey upbitKey = upbitKeyRepository.findByUser(user);
if (Objects.isNull(upbitKey)) {
throw new UserException(UserErrorResult.NOT_FOUND_KEY);
}

String token = jwtUtil.generateUpbitToken(upbitKey);
return upbitApiClient.getUserAccountsAtUpbit(UPBIT_URL_ACCOUNT, token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.dgu.backend.service;

import org.dgu.backend.domain.UpbitKey;

import java.util.Map;

public interface OrderService {
void executeOrder(Map<String, String> params, UpbitKey upbitKey);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.dgu.backend.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.dgu.backend.domain.UpbitKey;
import org.dgu.backend.util.HashUtil;
import org.dgu.backend.util.JwtUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
@Value("${upbit.url.order}")
private String UPBIT_URL_ORDER;
private final JwtUtil jwtUtil;
private final UpbitApiClient upbitApiClient;

// 주문을 진행하는 메서드
@Override
@Transactional
public void executeOrder(Map<String, String> params, UpbitKey upbitKey) {
String queryString = HashUtil.buildQueryString(params);
String queryHash = HashUtil.generateQueryHash(queryString);
String authenticationToken = jwtUtil.generateUpbitOrderToken(upbitKey, queryHash);
upbitApiClient.createNewOrder(UPBIT_URL_ORDER, authenticationToken, params);
System.out.println("자동매매에 성공했습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.dgu.backend.service;

import org.dgu.backend.domain.PortfolioOption;
import org.dgu.backend.domain.TradingOption;
import org.dgu.backend.domain.User;
import org.dgu.backend.dto.TradingDto;

public interface TradingService {
void registerAutoTrading(String authorizationHeader, TradingDto.AutoTradingRequest autoTradingRequest);
void removeAutoTrading(String authorizationHeader, String portfolioId);
void executeTrade(User user, PortfolioOption portfolioOption, TradingOption tradingOption, Double curPrice);
}
Loading

0 comments on commit 73b10d9

Please sign in to comment.