goldenChat base source add
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
frontend_golden/
|
||||
node_modules/
|
||||
.git/
|
||||
deploy-home.sh
|
||||
# ta4j-master 는 backend 빌드에 필요하므로 포함
|
||||
@@ -0,0 +1,17 @@
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# GoldenChart Docker Compose 환경 변수 예시
|
||||
# .env 파일로 복사 후 값 수정
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
# MySQL
|
||||
MYSQL_ROOT_PASSWORD=rootpassword
|
||||
MYSQL_DATABASE=stockAnalyzer
|
||||
MYSQL_USER=stock
|
||||
MYSQL_PASSWORD=analyzer
|
||||
MYSQL_PORT=3306
|
||||
|
||||
# Backend
|
||||
BACKEND_PORT=8080
|
||||
|
||||
# Frontend (외부 노출 포트)
|
||||
FRONTEND_PORT=80
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Dependencies & build
|
||||
**/node_modules/
|
||||
**/dist/
|
||||
**/target/
|
||||
**/.gradle/
|
||||
|
||||
# Environment (secrets)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE / OS
|
||||
.idea/
|
||||
.vscode/
|
||||
.cursor/
|
||||
**/.DS_Store
|
||||
**/__MACOSX/
|
||||
|
||||
# Logs & cache
|
||||
*.log
|
||||
.npm/
|
||||
.cache/
|
||||
|
||||
# Deploy / local
|
||||
*.tar.gz
|
||||
Binary file not shown.
+1133
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
target/
|
||||
*.log
|
||||
.mvn/
|
||||
@@ -0,0 +1,42 @@
|
||||
# ── 1단계: 빌드 ──────────────────────────────────────────────────────────────
|
||||
# ta4j-master 는 Java 25 기준으로 빌드되어 있으므로 Java 25 이미지 사용
|
||||
FROM maven:3.9-eclipse-temurin-21 AS builder
|
||||
WORKDIR /build
|
||||
|
||||
# ── ta4j-master 전체 복사 (라이선스 헤더 파일 등 상대경로 유지) ─────────────
|
||||
COPY ta4j-master ./ta4j-master
|
||||
|
||||
# 공통 skip 옵션 + 컴파일러 버전을 21로 강제 오버라이드
|
||||
ARG SKIP_OPTS="-Denforcer.skip=true -Dlicense.skip=true -Dcheckstyle.skip=true -Dspotbugs.skip=true -DskipTests -Dmaven.compiler.release=21"
|
||||
|
||||
# 부모 POM 설치 (-N: 하위 모듈 빌드 생략)
|
||||
RUN mvn -f ta4j-master/pom.xml -N install $SKIP_OPTS -q
|
||||
|
||||
# ta4j-core 만 빌드하여 로컬 리포지터리에 설치
|
||||
RUN mvn -f ta4j-master/ta4j-core/pom.xml install $SKIP_OPTS -q
|
||||
|
||||
# ── Backend 빌드 ─────────────────────────────────────────────────────────────
|
||||
# 의존성 레이어 캐시 (pom.xml 변경 시만 재다운로드)
|
||||
COPY backend/pom.xml ./backend/pom.xml
|
||||
RUN mvn -f backend/pom.xml dependency:go-offline -q 2>/dev/null || true
|
||||
|
||||
# 소스 빌드
|
||||
COPY backend/src ./backend/src
|
||||
RUN mvn -f backend/pom.xml package -DskipTests -q
|
||||
|
||||
# ── 2단계: 실행 ──────────────────────────────────────────────────────────────
|
||||
FROM eclipse-temurin:21-jre-alpine AS production
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup -S spring && adduser -S spring -G spring
|
||||
USER spring
|
||||
|
||||
COPY --from=builder /build/backend/target/goldenchart-backend-*.jar app.jar
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["java", \
|
||||
"-XX:+UseContainerSupport", \
|
||||
"-XX:MaxRAMPercentage=75.0", \
|
||||
"-Djava.security.egd=file:/dev/./urandom", \
|
||||
"-jar", "app.jar"]
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.goldenchart</groupId>
|
||||
<artifactId>goldenchart-backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>GoldenChart Backend</name>
|
||||
<description>GoldenChart Spring Boot Backend with Ta4j indicators</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<ta4j.version>0.22.7-SNAPSHOT</ta4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Data JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- WebSocket + STOMP Broker -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Java WebSocket client (Upbit WS 백엔드 수신) -->
|
||||
<dependency>
|
||||
<groupId>org.java-websocket</groupId>
|
||||
<artifactId>Java-WebSocket</artifactId>
|
||||
<version>1.5.7</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP client (Upbit REST 프록시) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Connector -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Ta4j - Technical Analysis for Java (로컬 빌드된 snapshot 사용) -->
|
||||
<dependency>
|
||||
<groupId>org.ta4j</groupId>
|
||||
<artifactId>ta4j-core</artifactId>
|
||||
<version>${ta4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ta4j 의존: slf4j, commons-math3 -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>3.6.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson (JSON) -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Rate limiter (주문 실행 큐) -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.3.1-jre</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Upbit Open API JWT -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Firebase Cloud Messaging (선택 — 서비스 계정 JSON 없으면 비활성) -->
|
||||
<dependency>
|
||||
<groupId>com.google.firebase</groupId>
|
||||
<artifactId>firebase-admin</artifactId>
|
||||
<version>9.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Flyway DB Migration -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.goldenchart;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class GoldenChartApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GoldenChartApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.goldenchart.auth;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class MenuIds {
|
||||
|
||||
private MenuIds() {}
|
||||
|
||||
public static final List<String> ALL = List.of(
|
||||
"dashboard", "chart", "paper", "strategy", "backtest", "notifications", "settings",
|
||||
"settings_general", "settings_chart", "settings_indicators", "settings_backtest",
|
||||
"settings_strategy", "settings_paper", "settings_alert", "settings_network", "settings_admin"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.goldenchart.auth;
|
||||
|
||||
public enum UserRole {
|
||||
ADMIN,
|
||||
USER,
|
||||
GUEST;
|
||||
|
||||
public static UserRole fromString(String value) {
|
||||
if (value == null || value.isBlank()) return USER;
|
||||
try {
|
||||
return UserRole.valueOf(value.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return USER;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import com.goldenchart.service.AuthService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AdminUserInitializer implements ApplicationRunner {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
authService.ensureDefaultAdmin();
|
||||
log.info("[Auth] default admin user ensured (username=admin)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "goldenchart.cors")
|
||||
public class CorsConfig {
|
||||
|
||||
private List<String> allowedOrigins = List.of("http://localhost:5173", "http://localhost:3000");
|
||||
|
||||
public List<String> getAllowedOrigins() {
|
||||
return allowedOrigins;
|
||||
}
|
||||
|
||||
public void setAllowedOrigins(List<String> allowedOrigins) {
|
||||
this.allowedOrigins = allowedOrigins;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
// setAllowedOriginPatterns 사용: wildcard(*) + allowCredentials 동시 지원
|
||||
if (allowedOrigins.contains("*")) {
|
||||
config.setAllowedOriginPatterns(List.of("*"));
|
||||
} else {
|
||||
config.setAllowedOriginPatterns(allowedOrigins);
|
||||
}
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.google.firebase.FirebaseApp;
|
||||
import com.google.firebase.FirebaseOptions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class FirebaseConfig {
|
||||
|
||||
@Value("${firebase.service-account-file:firebase-service-account.json}")
|
||||
private String serviceAccountFile;
|
||||
|
||||
@Value("${firebase.enabled:false}")
|
||||
private boolean firebaseEnabled;
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
if (!firebaseEnabled) {
|
||||
log.info("[Firebase] disabled (firebase.enabled=false)");
|
||||
return;
|
||||
}
|
||||
if (!FirebaseApp.getApps().isEmpty()) return;
|
||||
|
||||
try (InputStream in = openServiceAccount()) {
|
||||
if (in == null) {
|
||||
log.warn("[Firebase] service account not found — FCM disabled");
|
||||
return;
|
||||
}
|
||||
FirebaseOptions options = FirebaseOptions.builder()
|
||||
.setCredentials(GoogleCredentials.fromStream(in))
|
||||
.build();
|
||||
FirebaseApp.initializeApp(options);
|
||||
log.info("[Firebase] Admin SDK initialized — FCM enabled");
|
||||
} catch (Exception e) {
|
||||
log.warn("[Firebase] init failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream openServiceAccount() {
|
||||
try {
|
||||
var res = new ClassPathResource(serviceAccountFile);
|
||||
if (res.exists()) return res.getInputStream();
|
||||
} catch (Exception ignored) { }
|
||||
try {
|
||||
return new FileInputStream(serviceAccountFile);
|
||||
} catch (Exception ignored) { }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import com.goldenchart.entity.GcAppSettings;
|
||||
import com.goldenchart.entity.GcWatchlist;
|
||||
import com.goldenchart.repository.GcWatchlistRepository;
|
||||
import com.goldenchart.service.AppSettingsService;
|
||||
import com.goldenchart.service.LiveStrategyEvaluator;
|
||||
import com.goldenchart.websocket.DynamicSubscriptionManager;
|
||||
import com.goldenchart.websocket.UpbitWebSocketClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 서버 기동 시 isLiveCheck=true 인 모든 종목을 업비트 WS·Ta4jStorage 에 고정 구독한다.
|
||||
* (재시작 후에도 실시간 전략 평가가 끊기지 않도록)
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LiveStrategyStartupRunner implements ApplicationRunner {
|
||||
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final GcWatchlistRepository watchlistRepo;
|
||||
private final DynamicSubscriptionManager subscriptionManager;
|
||||
private final LiveStrategyEvaluator liveStrategyEvaluator;
|
||||
private final UpbitWebSocketClient upbitWebSocketClient;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
// 디바이스별 전역 설정이 켜져 있고 전략이 지정된 경우, DB 관심종목 전체 고정 구독
|
||||
int pinned = 0;
|
||||
for (GcWatchlist w : watchlistRepo.findAll()) {
|
||||
String deviceId = w.getDeviceId();
|
||||
if (deviceId == null) continue;
|
||||
GcAppSettings app = appSettingsService.getEntity(null, deviceId);
|
||||
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) || app.getLiveStrategyId() == null) {
|
||||
continue;
|
||||
}
|
||||
subscriptionManager.ensureMarketPinned(w.getSymbol(), "1m");
|
||||
liveStrategyEvaluator.invalidateCache(w.getSymbol());
|
||||
pinned++;
|
||||
}
|
||||
if (pinned > 0) {
|
||||
log.info("[LiveStrategyStartup] 관심종목 기준 마켓 {}개 고정 구독 완료", pinned);
|
||||
}
|
||||
// WS 연결 직후 addMarket 이 구독 패킷을 놓치는 경우 대비
|
||||
upbitWebSocketClient.resubscribeAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http, CorsConfig corsConfig) throws Exception {
|
||||
http
|
||||
.cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 개발 단계: 전체 허용 (운영 시 JWT 필터 추가)
|
||||
.requestMatchers("/**").permitAll()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import com.goldenchart.entity.GcAppSettings;
|
||||
import com.goldenchart.repository.GcAppSettingsRepository;
|
||||
import com.goldenchart.security.SecretCryptoService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 기존 평문 업비트 API 키를 AES 암호문으로 일괄 변환 (1회성·멱등).
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class UpbitKeyEncryptionMigrator implements ApplicationRunner {
|
||||
|
||||
private final GcAppSettingsRepository repo;
|
||||
private final SecretCryptoService secretCrypto;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(ApplicationArguments args) {
|
||||
int migrated = 0;
|
||||
for (GcAppSettings s : repo.findAll()) {
|
||||
boolean changed = false;
|
||||
String access = s.getUpbitAccessKey();
|
||||
if (access != null && !access.isBlank() && !secretCrypto.isEncrypted(access)) {
|
||||
s.setUpbitAccessKey(secretCrypto.encrypt(access));
|
||||
changed = true;
|
||||
}
|
||||
String secret = s.getUpbitSecretKey();
|
||||
if (secret != null && !secret.isBlank() && !secretCrypto.isEncrypted(secret)) {
|
||||
s.setUpbitSecretKey(secretCrypto.encrypt(secret));
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
repo.save(s);
|
||||
migrated++;
|
||||
}
|
||||
}
|
||||
if (migrated > 0) {
|
||||
log.info("[UpbitKeyEncryptionMigrator] {} settings row(s) API keys encrypted", migrated);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* STOMP WebSocket 브로커 설정.
|
||||
*
|
||||
* 클라이언트 연결 엔드포인트:
|
||||
* ws(s)://host/ws/trading (SockJS fallback 지원)
|
||||
*
|
||||
* 구독 채널 (명세서 5.2):
|
||||
* /sub/charts/{market}/{type} — 실시간 캔들 배포
|
||||
*
|
||||
* 발행 채널:
|
||||
* /pub/... — 클라이언트 → 서버 (향후 확장용)
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
// 인메모리 심플 브로커: /sub 접두사 구독 채널에 메시지 라우팅
|
||||
registry.enableSimpleBroker("/sub");
|
||||
// 클라이언트 발행 경로 접두사 (서버측 @MessageMapping 핸들러로 라우팅)
|
||||
registry.setApplicationDestinationPrefixes("/pub");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws/trading")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.withSockJS(); // SockJS fallback (브라우저 환경 대비)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.*;
|
||||
import com.goldenchart.service.AdminService;
|
||||
import com.goldenchart.service.RolePermissionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/admin")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminController {
|
||||
|
||||
private final AdminService adminService;
|
||||
private final RolePermissionService rolePermissionService;
|
||||
|
||||
private Long requireUserId(@RequestHeader(value = "X-User-Id", required = false) Long userId) {
|
||||
if (userId == null) {
|
||||
throw new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
|
||||
}
|
||||
rolePermissionService.requireAdmin(userId);
|
||||
return userId;
|
||||
}
|
||||
|
||||
@PostMapping("/verify-password")
|
||||
public ResponseEntity<Map<String, Boolean>> verifyPassword(
|
||||
@RequestHeader("X-User-Id") Long userId,
|
||||
@RequestBody AdminVerifyRequest body) {
|
||||
adminService.verifyAdminPassword(userId, body != null ? body.getPassword() : null);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@GetMapping("/users")
|
||||
public List<UserDto> listUsers(@RequestHeader("X-User-Id") Long userId) {
|
||||
requireUserId(userId);
|
||||
return adminService.listUsers();
|
||||
}
|
||||
|
||||
@PostMapping("/users")
|
||||
public UserDto createUser(
|
||||
@RequestHeader("X-User-Id") Long userId,
|
||||
@RequestBody CreateUserRequest body) {
|
||||
requireUserId(userId);
|
||||
return adminService.createUser(body);
|
||||
}
|
||||
|
||||
@PutMapping("/users/{id}")
|
||||
public UserDto updateUser(
|
||||
@RequestHeader("X-User-Id") Long userId,
|
||||
@PathVariable Long id,
|
||||
@RequestBody UpdateUserRequest body) {
|
||||
requireUserId(userId);
|
||||
return adminService.updateUser(id, body, userId);
|
||||
}
|
||||
|
||||
@DeleteMapping("/users/{id}")
|
||||
public ResponseEntity<Void> deleteUser(
|
||||
@RequestHeader("X-User-Id") Long userId,
|
||||
@PathVariable Long id) {
|
||||
requireUserId(userId);
|
||||
adminService.deleteUser(id, userId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/role-permissions")
|
||||
public Map<String, Map<String, Boolean>> listRolePermissions(
|
||||
@RequestHeader("X-User-Id") Long userId) {
|
||||
requireUserId(userId);
|
||||
return rolePermissionService.listAllRolePermissions();
|
||||
}
|
||||
|
||||
@PutMapping("/role-permissions")
|
||||
public ResponseEntity<Void> updateRolePermissions(
|
||||
@RequestHeader("X-User-Id") Long userId,
|
||||
@RequestBody RolePermissionsUpdateRequest body) {
|
||||
requireUserId(userId);
|
||||
rolePermissionService.updateRolePermissions(body);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.service.AppSettingsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 앱 전역 차트 기본 설정 REST API.
|
||||
*
|
||||
* <p>프론트엔드에 하드코딩된 기본값들(기본 심볼·타임프레임·테마·캔들 색상 등)을
|
||||
* 장치/사용자별로 DB 에서 관리한다.</p>
|
||||
*
|
||||
* <pre>
|
||||
* GET /app-settings — 현재 기본 설정 로드 (앱 시작 시 호출)
|
||||
* PUT /app-settings — 기본 설정 저장
|
||||
* </pre>
|
||||
*
|
||||
* 반환 구조:
|
||||
* <pre>
|
||||
* {
|
||||
* "defaultSymbol": "KRW-BTC",
|
||||
* "defaultTimeframe": "1D",
|
||||
* "defaultChartType": "candlestick",
|
||||
* "defaultTheme": "dark",
|
||||
* "defaultLogScale": false,
|
||||
* "defaultLayoutId": "1",
|
||||
* "mainChartStyle": { "upColor":"#ff6b6b", "downColor":"#4dabf7", ... },
|
||||
* "syncOptions": { "symbol":false, "interval":false, "crosshair":true, ... }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/app-settings")
|
||||
@RequiredArgsConstructor
|
||||
public class AppSettingsController {
|
||||
|
||||
private final AppSettingsService service;
|
||||
|
||||
// ── GET ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 앱 시작 시 기본 설정 로드.
|
||||
* DB 에 데이터가 없으면 엔티티 기본값(hardcoded fallback)을 반환한다.
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> get(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
return ResponseEntity.ok(service.get(parseUserId(userIdHeader), deviceId));
|
||||
}
|
||||
|
||||
// ── PUT ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 기본 설정 저장.
|
||||
* 변경된 필드만 보내도 되며, 없는 키는 기존 값 유지.
|
||||
*
|
||||
* <p>프론트엔드에서 호출해야 하는 시점:
|
||||
* <ul>
|
||||
* <li>테마 변경 시 (defaultTheme)</li>
|
||||
* <li>캔들 색상 변경 시 (mainChartStyle)</li>
|
||||
* <li>레이아웃 변경 시 (defaultLayoutId)</li>
|
||||
* <li>기본 심볼/타임프레임 변경 시</li>
|
||||
* <li>동기화 옵션 변경 시 (syncOptions)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@PutMapping
|
||||
public ResponseEntity<Map<String, Object>> save(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId,
|
||||
@RequestBody Map<String, Object> data) {
|
||||
return ResponseEntity.ok(service.save(parseUserId(userIdHeader), deviceId, data));
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private Long parseUserId(String header) {
|
||||
if (header == null || header.isBlank()) return null;
|
||||
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.LoginRequest;
|
||||
import com.goldenchart.dto.LoginResponse;
|
||||
import com.goldenchart.dto.MenuPermissionsResponse;
|
||||
import com.goldenchart.service.AuthService;
|
||||
import com.goldenchart.service.RolePermissionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
private final RolePermissionService rolePermissionService;
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest body) {
|
||||
return ResponseEntity.ok(authService.login(body.getUsername(), body.getPassword()));
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<LoginResponse> me(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader) {
|
||||
Long userId = parseUserId(userIdHeader);
|
||||
return authService.me(userId)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/** 로그인 사용자 역할·메뉴 권한 (미로그인 시 GUEST) */
|
||||
@GetMapping("/menu-permissions")
|
||||
public MenuPermissionsResponse menuPermissions(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader) {
|
||||
return rolePermissionService.resolveForUserId(parseUserId(userIdHeader));
|
||||
}
|
||||
|
||||
private Long parseUserId(String header) {
|
||||
if (header == null || header.isBlank()) return null;
|
||||
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.entity.GcBacktestResult;
|
||||
import com.goldenchart.repository.GcBacktestResultRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 백테스팅 실행 이력 조회 API.
|
||||
*
|
||||
* <pre>
|
||||
* GET /backtest-results?deviceId=xxx → 이력 목록 (최신순)
|
||||
* GET /backtest-results/{id} → 단건 상세 조회
|
||||
* DELETE /backtest-results/{id} → 삭제
|
||||
* </pre>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/backtest-results")
|
||||
@RequiredArgsConstructor
|
||||
public class BacktestResultController {
|
||||
|
||||
private final GcBacktestResultRepository resultRepository;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<GcBacktestResult>> list(
|
||||
@RequestParam(required = false, defaultValue = "default") String deviceId) {
|
||||
return ResponseEntity.ok(resultRepository.findByDeviceIdOrderByCreatedAtDesc(deviceId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<GcBacktestResult> get(@PathVariable Long id) {
|
||||
return resultRepository.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||
if (!resultRepository.existsById(id)) return ResponseEntity.notFound().build();
|
||||
resultRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.BacktestSettingsDto;
|
||||
import com.goldenchart.service.BacktestSettingsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/backtest-settings")
|
||||
@RequiredArgsConstructor
|
||||
@CrossOrigin(origins = "*")
|
||||
public class BacktestSettingsController {
|
||||
|
||||
private final BacktestSettingsService service;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<BacktestSettingsDto> get(
|
||||
@RequestHeader(value = "X-Device-Id", defaultValue = "default") String deviceId) {
|
||||
return ResponseEntity.ok(service.get(deviceId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<BacktestSettingsDto> save(
|
||||
@RequestHeader(value = "X-Device-Id", defaultValue = "default") String deviceId,
|
||||
@RequestBody BacktestSettingsDto dto) {
|
||||
return ResponseEntity.ok(service.save(deviceId, dto));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.BacktestRequest;
|
||||
import com.goldenchart.dto.BacktestResponse;
|
||||
import com.goldenchart.service.BacktestingService;
|
||||
import com.goldenchart.service.IndicatorSettingsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 백테스팅 실행 REST API.
|
||||
*
|
||||
* <p>POST /backtesting/run 에 BacktestRequest 를 전송하면
|
||||
* Ta4j 로 백테스팅을 수행하여 매수/매도 시그널과 통계를 반환합니다.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/backtesting")
|
||||
@RequiredArgsConstructor
|
||||
public class BacktestingController {
|
||||
|
||||
private final BacktestingService backtestingService;
|
||||
private final IndicatorSettingsService indicatorSettingsService;
|
||||
|
||||
/**
|
||||
* 백테스팅 실행.
|
||||
*
|
||||
* <p>요청 예시:
|
||||
* <pre>
|
||||
* POST /backtesting/run
|
||||
* {
|
||||
* "strategyId": 1, // DB 전략 ID (선택)
|
||||
* "buyCondition": {...}, // DSL 직접 전달 시 (strategyId 없을 때 사용)
|
||||
* "sellCondition": {...},
|
||||
* "bars": [{time, open, high, low, close, volume}, ...],
|
||||
* "timeframe": "1D"
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@PostMapping("/run")
|
||||
public ResponseEntity<BacktestResponse> run(
|
||||
@RequestBody BacktestRequest req,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
|
||||
if (req.getBars() == null || req.getBars().isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Long userId = parseUserId(userIdHeader);
|
||||
|
||||
// DB 저장 파라미터를 기반으로, 요청 params 로 덮어쓰기
|
||||
// (요청에 params 없으면 DB 값만 사용 → 사용자가 변경한 파라미터 항상 반영)
|
||||
if (userId != null || deviceId != null) {
|
||||
java.util.Map<String, java.util.Map<String, Object>> dbParams =
|
||||
indicatorSettingsService.getAll(userId, deviceId);
|
||||
if (!dbParams.isEmpty()) {
|
||||
java.util.Map<String, java.util.Map<String, Object>> merged =
|
||||
new java.util.HashMap<>(dbParams);
|
||||
if (req.getIndicatorParams() != null) {
|
||||
req.getIndicatorParams().forEach((type, params) ->
|
||||
merged.merge(type, params, (dbP, reqP) -> {
|
||||
java.util.Map<String, Object> m = new java.util.HashMap<>(dbP);
|
||||
m.putAll(reqP);
|
||||
return m;
|
||||
})
|
||||
);
|
||||
}
|
||||
req.setIndicatorParams(merged);
|
||||
}
|
||||
}
|
||||
|
||||
BacktestResponse response = backtestingService.run(req);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
private Long parseUserId(String header) {
|
||||
if (header == null || header.isBlank()) return null;
|
||||
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.CandleBarDto;
|
||||
import com.goldenchart.service.HistoricalDataService;
|
||||
import com.goldenchart.websocket.DynamicSubscriptionManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 캔들 데이터 REST API 컨트롤러.
|
||||
*
|
||||
* context-path: /api
|
||||
* endpoint: GET /api/candles/history
|
||||
*
|
||||
* 명세서 5.1 포맷:
|
||||
* Query Params: market=KRW-BTC, type=1m, to=2026-05-20T10:17:00Z, count=200
|
||||
* Response: CandleBarDto[] (time, open, high, low, close, volume, rsi)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/candles")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CandleController {
|
||||
|
||||
private final HistoricalDataService historicalDataService;
|
||||
private final DynamicSubscriptionManager subscriptionManager;
|
||||
|
||||
/**
|
||||
* 과거 캔들 데이터 조회.
|
||||
*
|
||||
* @param market 업비트 마켓 코드 (e.g. "KRW-BTC")
|
||||
* @param type 캔들 타입 (e.g. "1m", "5m", "1h", "1d")
|
||||
* @param to 기준 시간 이전(exclusive) ISO-8601 (e.g. "2026-05-20T10:00:00" 또는 "...Z")
|
||||
* @param count 요청 캔들 수 (기본값 200, 최대 200)
|
||||
* @return 시간 오름차순 CandleBarDto 배열
|
||||
*/
|
||||
@GetMapping("/history")
|
||||
public ResponseEntity<List<CandleBarDto>> getHistory(
|
||||
@RequestParam String market,
|
||||
@RequestParam(defaultValue = "1d") String type,
|
||||
@RequestParam(required = false) String to,
|
||||
@RequestParam(defaultValue = "200") int count) {
|
||||
|
||||
log.info("[CandleController] history 요청: market={}, type={}, to={}, count={}", market, type, to, count);
|
||||
List<CandleBarDto> bars = historicalDataService.getHistory(market, type, to, count);
|
||||
return ResponseEntity.ok(bars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차트 실시간용 — 업비트 WS·Ta4j warm-up 고정 (STOMP 구독과 병행).
|
||||
*/
|
||||
@PostMapping("/watch")
|
||||
public ResponseEntity<Void> watch(
|
||||
@RequestParam String market,
|
||||
@RequestParam(defaultValue = "1m") String type) {
|
||||
subscriptionManager.ensureMarketPinned(market, type);
|
||||
subscriptionManager.ensureMarketPinned(market, "1m");
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.goldenchart.entity.GcChartSlot;
|
||||
import com.goldenchart.entity.GcChartWorkspace;
|
||||
import com.goldenchart.service.ChartSettingsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 차트 워크스페이스 설정 REST API.
|
||||
*
|
||||
* GET /api/chart/workspace - 전체 설정 로드 (프론트 시작 시 호출)
|
||||
* POST /api/chart/workspace - 전체 설정 저장
|
||||
* PUT /api/chart/workspace/layout - 레이아웃만 변경
|
||||
* PUT /api/chart/slot/{index} - 특정 슬롯 저장
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/chart")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ChartSettingsController {
|
||||
|
||||
private final ChartSettingsService settingsService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/** 헤더에서 userId 또는 deviceId 추출 */
|
||||
private Long userId(Map<String, String> headers) {
|
||||
String v = headers.get("x-user-id");
|
||||
return v != null ? Long.parseLong(v) : null;
|
||||
}
|
||||
private String deviceId(Map<String, String> headers) {
|
||||
return headers.getOrDefault("x-device-id", "anonymous");
|
||||
}
|
||||
|
||||
/**
|
||||
* 프론트엔드 시작 시 전체 워크스페이스 로드.
|
||||
* DB에 저장된 설정이 없으면 204 반환 → 프론트는 기본값 사용.
|
||||
*/
|
||||
@GetMapping("/workspace")
|
||||
public ResponseEntity<?> getWorkspace(
|
||||
@RequestHeader Map<String, String> headers) {
|
||||
GcChartWorkspace ws = settingsService.getWorkspaceWithSlots(
|
||||
userId(headers), deviceId(headers));
|
||||
|
||||
if (ws == null) return ResponseEntity.noContent().build();
|
||||
|
||||
return ResponseEntity.ok(toDto(ws));
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 워크스페이스 저장 (자동 저장 / 명시 저장).
|
||||
*/
|
||||
@PostMapping("/workspace")
|
||||
public ResponseEntity<?> saveWorkspace(
|
||||
@RequestHeader Map<String, String> headers,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
Long uid = userId(headers);
|
||||
String did = deviceId(headers);
|
||||
String layoutId = (String) body.getOrDefault("layoutId", "1");
|
||||
String syncJson = body.containsKey("syncOptions")
|
||||
? toJson(body.get("syncOptions")) : null;
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> slotsData = (List<Map<String, Object>>) body.get("slots");
|
||||
|
||||
GcChartWorkspace ws = settingsService.saveAll(uid, did, layoutId, syncJson,
|
||||
slotsData != null ? slotsData : List.of());
|
||||
return ResponseEntity.ok(toDto(ws));
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃만 변경.
|
||||
*/
|
||||
@PutMapping("/workspace/layout")
|
||||
public ResponseEntity<?> updateLayout(
|
||||
@RequestHeader Map<String, String> headers,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
String layoutId = (String) body.get("layoutId");
|
||||
String syncJson = body.containsKey("syncOptions") ? toJson(body.get("syncOptions")) : null;
|
||||
GcChartWorkspace ws = settingsService.saveWorkspace(
|
||||
userId(headers), deviceId(headers), layoutId, syncJson);
|
||||
return ResponseEntity.ok(Map.of("layoutId", ws.getLayoutId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 슬롯 설정 저장.
|
||||
*/
|
||||
@PutMapping("/slot/{slotIndex}")
|
||||
public ResponseEntity<?> saveSlot(
|
||||
@RequestHeader Map<String, String> headers,
|
||||
@PathVariable int slotIndex,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
GcChartSlot slot = settingsService.saveSlot(
|
||||
userId(headers), deviceId(headers), slotIndex, body);
|
||||
return ResponseEntity.ok(slotToDto(slot));
|
||||
}
|
||||
|
||||
// ── DTO 변환 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private Map<String, Object> toDto(GcChartWorkspace ws) {
|
||||
Map<String, Object> dto = new HashMap<>();
|
||||
dto.put("id", ws.getId());
|
||||
dto.put("layoutId", ws.getLayoutId());
|
||||
dto.put("syncOptions", parseJson(ws.getSyncOptionsJson()));
|
||||
dto.put("slots", ws.getSlots().stream()
|
||||
.map(this::slotToDto)
|
||||
.collect(Collectors.toList()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private Map<String, Object> slotToDto(GcChartSlot s) {
|
||||
Map<String, Object> dto = new HashMap<>();
|
||||
dto.put("slotIndex", s.getSlotIndex());
|
||||
dto.put("symbol", s.getSymbol());
|
||||
dto.put("timeframe", s.getTimeframe());
|
||||
dto.put("chartType", s.getChartType());
|
||||
dto.put("theme", s.getTheme());
|
||||
dto.put("mode", s.getMode());
|
||||
dto.put("logScale", s.getLogScale());
|
||||
dto.put("drawingsLocked", s.getDrawingsLocked());
|
||||
dto.put("drawingsVisible", s.getDrawingsVisible());
|
||||
dto.put("indicators", parseJson(s.getIndicatorsJson()));
|
||||
dto.put("drawings", parseJson(s.getDrawingsJson()));
|
||||
dto.put("paneLayout", parseJson(s.getPaneLayoutJson()));
|
||||
dto.put("mainChartStyle", parseJson(s.getMainChartStyleJson()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private Object parseJson(String json) {
|
||||
if (json == null) return null;
|
||||
try { return objectMapper.readValue(json, Object.class); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
private String toJson(Object obj) {
|
||||
try { return objectMapper.writeValueAsString(obj); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.service.DashboardService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
@GetMapping("/summary")
|
||||
public ResponseEntity<Map<String, Object>> summary(@RequestHeader Map<String, String> h) {
|
||||
Long userId = null;
|
||||
String uid = h.get("x-user-id");
|
||||
if (uid != null && !uid.isBlank()) {
|
||||
try { userId = Long.parseLong(uid); } catch (NumberFormatException ignored) { }
|
||||
}
|
||||
String deviceId = h.getOrDefault("x-device-id", "anonymous");
|
||||
return ResponseEntity.ok(dashboardService.buildSummary(userId, deviceId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.FcmTokenDto;
|
||||
import com.goldenchart.service.FcmPushService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/fcm")
|
||||
@RequiredArgsConstructor
|
||||
public class FcmController {
|
||||
|
||||
private final FcmPushService fcmPushService;
|
||||
|
||||
private Long userId(Map<String, String> h) {
|
||||
String v = h.get("x-user-id");
|
||||
return v != null ? Long.parseLong(v) : null;
|
||||
}
|
||||
|
||||
private String deviceId(Map<String, String> h) {
|
||||
return h.getOrDefault("x-device-id", "anonymous");
|
||||
}
|
||||
|
||||
@PostMapping("/token")
|
||||
public ResponseEntity<Void> register(@RequestHeader Map<String, String> h,
|
||||
@RequestBody FcmTokenDto body) {
|
||||
fcmPushService.registerToken(
|
||||
body.getUserId() != null ? body.getUserId() : userId(h),
|
||||
body.getDeviceId() != null ? body.getDeviceId() : deviceId(h),
|
||||
body.getToken());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/token")
|
||||
public ResponseEntity<Void> delete(@RequestHeader Map<String, String> h) {
|
||||
fcmPushService.deleteByDevice(deviceId(h));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/test")
|
||||
public ResponseEntity<Map<String, Object>> test(@RequestHeader Map<String, String> h) {
|
||||
fcmPushService.sendTest(userId(h), deviceId(h));
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"ok", true,
|
||||
"available", fcmPushService.isAvailable()
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
public ResponseEntity<Map<String, Object>> status() {
|
||||
return ResponseEntity.ok(Map.of("available", fcmPushService.isAvailable()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.IndicatorRequest;
|
||||
import com.goldenchart.dto.IndicatorResponse;
|
||||
import com.goldenchart.service.IndicatorService;
|
||||
import com.goldenchart.service.IndicatorSettingsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 지표 계산 REST API.
|
||||
*
|
||||
* <p>파라미터 우선순위 (높은 순):
|
||||
* <ol>
|
||||
* <li>요청 body 의 params (프론트엔드가 명시적으로 보낸 값)</li>
|
||||
* <li>DB gc_indicator_settings 에 저장된 사용자 설정값</li>
|
||||
* <li>IndicatorService 내부 코드 기본값 (최후 fallback)</li>
|
||||
* </ol>
|
||||
* 이 순서로 병합하여 프론트엔드와 백엔드가 항상 동일한 파라미터로 계산된다.
|
||||
* </p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/indicators")
|
||||
@RequiredArgsConstructor
|
||||
public class IndicatorController {
|
||||
|
||||
private final IndicatorService indicatorService;
|
||||
private final IndicatorSettingsService settingsService;
|
||||
|
||||
/**
|
||||
* 지표값 계산.
|
||||
*
|
||||
* <p>헤더:
|
||||
* <ul>
|
||||
* <li>X-Device-Id — 비회원 기기 식별자</li>
|
||||
* <li>X-User-Id — 회원 ID (선택)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@PostMapping("/calculate")
|
||||
public ResponseEntity<IndicatorResponse> calculate(
|
||||
@RequestBody IndicatorRequest req,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
|
||||
if (req.getBars() == null || req.getBars().isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Long userId = parseUserId(userIdHeader);
|
||||
|
||||
/*
|
||||
* DB 설정값 + 요청 파라미터 병합.
|
||||
* - DB 값 기반으로 시작
|
||||
* - 요청 params 는 DB 값 위에 덮어쓰기 (명시적 전송이 더 최신)
|
||||
*/
|
||||
Map<String, Object> mergedParams = settingsService.mergeWithDb(
|
||||
userId, deviceId,
|
||||
req.getParams(),
|
||||
req.getIndicatorType());
|
||||
|
||||
IndicatorResponse response = indicatorService.calculate(
|
||||
req.getBars(), req.getIndicatorType(), mergedParams, req.getTimeframe());
|
||||
response.setSymbol(req.getSymbol());
|
||||
response.setTimeframe(req.getTimeframe());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 지원 지표 목록 반환.
|
||||
* frontend indicatorRegistry.ts 의 INDICATOR_REGISTRY 와 동기화.
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<?> list() {
|
||||
return ResponseEntity.ok(java.util.Map.of(
|
||||
"supported", java.util.List.of(
|
||||
"SMA", "EMA", "WMA", "HMA", "VWMA", "DEMA", "TEMA", "RMA", "SMMA", "LSMA", "MACross",
|
||||
"BollingerBands", "KeltnerChannels", "DonchianChannels", "BBPercentB", "BBBandWidth",
|
||||
"RSI", "Stochastic", "StochRSI", "CCI", "WilliamsPercentRange",
|
||||
"AwesomeOscillator", "DPO",
|
||||
"MACD", "Momentum", "ROC", "TSI",
|
||||
"ADX", "DMI", "Aroon", "IchimokuCloud", "ParabolicSAR",
|
||||
"Choppiness", "MassIndex",
|
||||
"ATR", "StandardDeviation", "HistoricalVolatility",
|
||||
"OBV", "MFI", "ChaikinMF",
|
||||
"VolumeOscillator", "VR", "Disparity",
|
||||
"Psychological", "InvestPsychological",
|
||||
"TRIX", "ADX", "DMI"
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private Long parseUserId(String header) {
|
||||
if (header == null || header.isBlank()) return null;
|
||||
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.service.IndicatorSettingsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 전역 지표 파라미터 CRUD REST API.
|
||||
*
|
||||
* <p>프론트엔드 indicatorRegistry.ts 의 defaultParams 를 완전히 대체하는
|
||||
* 사용자별 지표 파라미터를 저장/조회한다.</p>
|
||||
*
|
||||
* <pre>
|
||||
* GET /indicator-settings — 전체 지표 파라미터 조회
|
||||
* GET /indicator-settings/{type} — 특정 지표 파라미터 조회
|
||||
* PUT /indicator-settings — 전체 지표 파라미터 저장 (덮어쓰기)
|
||||
* PATCH /indicator-settings/{type} — 특정 지표 파라미터 병합 저장
|
||||
* </pre>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/indicator-settings")
|
||||
@RequiredArgsConstructor
|
||||
public class IndicatorSettingsController {
|
||||
|
||||
private final IndicatorSettingsService service;
|
||||
|
||||
// ── 헤더 파싱 헬퍼 ────────────────────────────────────────────────────────
|
||||
|
||||
private Long userId(String userIdHeader) {
|
||||
if (userIdHeader == null || userIdHeader.isBlank()) return null;
|
||||
try { return Long.parseLong(userIdHeader); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
|
||||
// ── GET: 전체 파라미터 조회 ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 이 장치/사용자의 모든 지표 파라미터를 반환.
|
||||
* 프론트엔드 앱 시작 시 호출하여 전역 설정을 로드한다.
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Map<String, Object>>> getAll(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
Map<String, Map<String, Object>> result = service.getAll(userId(userIdHeader), deviceId);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── GET: 특정 지표 파라미터 조회 ──────────────────────────────────────────
|
||||
|
||||
@GetMapping("/{indicatorType}")
|
||||
public ResponseEntity<Map<String, Object>> getForType(
|
||||
@PathVariable String indicatorType,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
return ResponseEntity.ok(service.getForType(userId(userIdHeader), deviceId, indicatorType));
|
||||
}
|
||||
|
||||
// ── PUT: 전체 파라미터 저장 ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 모든 지표 파라미터를 한 번에 저장(덮어쓰기).
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* "RSI": {"length": 9, "src": "close"},
|
||||
* "MACD": {"fastLength": 12, "slowLength": 26, "signalLength": 9},
|
||||
* "BollingerBands":{"length": 20, "mult": 2.0},
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
@PutMapping
|
||||
public ResponseEntity<Void> saveAll(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId,
|
||||
@RequestBody Map<String, Map<String, Object>> allParams) {
|
||||
service.saveAll(userId(userIdHeader), deviceId, allParams);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── PATCH: 특정 지표 파라미터 병합 저장 ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 특정 지표의 파라미터만 업데이트.
|
||||
* 나머지 지표 설정은 유지된다.
|
||||
*
|
||||
* PATCH /indicator-settings/RSI
|
||||
* Body: {"length": 14, "src": "close"}
|
||||
*/
|
||||
@PatchMapping("/{indicatorType}")
|
||||
public ResponseEntity<Void> saveForType(
|
||||
@PathVariable String indicatorType,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId,
|
||||
@RequestBody Map<String, Object> params) {
|
||||
service.saveForType(userId(userIdHeader), deviceId, indicatorType, params);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── GET: 전체 시각 설정 조회 ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 모든 지표의 시각 설정(색상·선굵기·수평선)을 반환.
|
||||
* 프론트엔드 앱 시작 시 호출하여 전역 시각 설정을 로드한다.
|
||||
*
|
||||
* GET /indicator-settings/visual
|
||||
*/
|
||||
@GetMapping("/visual")
|
||||
public ResponseEntity<Map<String, Map<String, Object>>> getAllVisual(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
return ResponseEntity.ok(service.getAllVisual(userId(userIdHeader), deviceId));
|
||||
}
|
||||
|
||||
// ── PATCH: 특정 지표 시각 설정 저장 ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 특정 지표의 시각 설정(색상·선굵기·수평선)을 병합 저장.
|
||||
*
|
||||
* PATCH /indicator-settings/RSI/visual
|
||||
* Body: {
|
||||
* "plots": [{"id":"plot0","color":"#ff0000","lineWidth":2,"type":"line"}],
|
||||
* "hlines": [{"price":70,"color":"#EF5350","visible":true}]
|
||||
* }
|
||||
*/
|
||||
@PatchMapping("/{indicatorType}/visual")
|
||||
public ResponseEntity<Void> saveVisualForType(
|
||||
@PathVariable String indicatorType,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId,
|
||||
@RequestBody Map<String, Object> visual) {
|
||||
service.saveVisualForType(userId(userIdHeader), deviceId, indicatorType, visual);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.LiveStrategyBulkRequest;
|
||||
import com.goldenchart.dto.LiveStrategySettingsDto;
|
||||
import com.goldenchart.service.LiveStrategySettingsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 실시간 전략 체크 설정 REST API.
|
||||
*
|
||||
* <pre>
|
||||
* GET /api/strategy/settings?market=KRW-BTC → 현재 설정 조회
|
||||
* PUT /api/strategy/settings → 설정 저장/갱신
|
||||
* </pre>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/strategy/settings")
|
||||
@RequiredArgsConstructor
|
||||
public class LiveStrategySettingsController {
|
||||
|
||||
private final LiveStrategySettingsService service;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<LiveStrategySettingsDto> get(
|
||||
@RequestParam(value = "market", defaultValue = "KRW-BTC") String market,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
|
||||
return ResponseEntity.ok(service.get(parseUserId(userIdHeader), deviceId, market));
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public ResponseEntity<LiveStrategySettingsDto> put(
|
||||
@RequestBody LiveStrategySettingsDto dto,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
|
||||
return ResponseEntity.ok(service.save(parseUserId(userIdHeader), deviceId, dto));
|
||||
}
|
||||
|
||||
/** 실시간 체크 ON + 전략 지정된 종목 목록 (현재 디바이스/유저) */
|
||||
@GetMapping("/active")
|
||||
public ResponseEntity<List<LiveStrategySettingsDto>> listActive(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
|
||||
return ResponseEntity.ok(service.listActive(parseUserId(userIdHeader), deviceId));
|
||||
}
|
||||
|
||||
/** 관심종목 등 여러 마켓에 동일 설정 일괄 저장 */
|
||||
@PutMapping("/bulk")
|
||||
public ResponseEntity<List<LiveStrategySettingsDto>> putBulk(
|
||||
@RequestBody LiveStrategyBulkRequest req,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
|
||||
return ResponseEntity.ok(service.saveBulk(parseUserId(userIdHeader), deviceId, req));
|
||||
}
|
||||
|
||||
private Long parseUserId(String header) {
|
||||
if (header == null || header.isBlank()) return null;
|
||||
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.LiveOrderRequest;
|
||||
import com.goldenchart.dto.LiveOrderRequest;
|
||||
import com.goldenchart.dto.LiveSummaryDto;
|
||||
import com.goldenchart.dto.LiveTradeDto;
|
||||
import com.goldenchart.service.LiveTradingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/live")
|
||||
@RequiredArgsConstructor
|
||||
public class LiveTradingController {
|
||||
|
||||
private final LiveTradingService liveTradingService;
|
||||
|
||||
@GetMapping("/summary")
|
||||
public ResponseEntity<LiveSummaryDto> summary(@RequestHeader Map<String, String> h) {
|
||||
return ResponseEntity.ok(liveTradingService.getSummary(userId(h), deviceId(h)));
|
||||
}
|
||||
|
||||
@GetMapping("/trades")
|
||||
public ResponseEntity<List<LiveTradeDto>> trades(@RequestHeader Map<String, String> h) {
|
||||
return ResponseEntity.ok(liveTradingService.listTrades(userId(h), deviceId(h)));
|
||||
}
|
||||
|
||||
@PostMapping("/orders")
|
||||
public ResponseEntity<LiveTradeDto> placeOrder(@RequestHeader Map<String, String> h,
|
||||
@RequestBody LiveOrderRequest body) {
|
||||
return ResponseEntity.ok(liveTradingService.placeManualOrder(userId(h), deviceId(h), body));
|
||||
}
|
||||
|
||||
private Long userId(Map<String, String> h) {
|
||||
String v = h.get("x-user-id");
|
||||
if (v == null || v.isBlank()) return null;
|
||||
try { return Long.parseLong(v); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
|
||||
private String deviceId(Map<String, String> h) {
|
||||
return h.getOrDefault("x-device-id", "anonymous");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.PaperOrderRequest;
|
||||
import com.goldenchart.dto.PaperSummaryDto;
|
||||
import com.goldenchart.dto.PaperTradeDto;
|
||||
import com.goldenchart.service.PaperTradingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/paper")
|
||||
@RequiredArgsConstructor
|
||||
public class PaperTradingController {
|
||||
|
||||
private final PaperTradingService paperTradingService;
|
||||
|
||||
private Long userId(Map<String, String> h) {
|
||||
String v = h.get("x-user-id");
|
||||
return v != null ? Long.parseLong(v) : null;
|
||||
}
|
||||
|
||||
private String deviceId(Map<String, String> h) {
|
||||
return h.getOrDefault("x-device-id", "anonymous");
|
||||
}
|
||||
|
||||
@GetMapping("/summary")
|
||||
public ResponseEntity<PaperSummaryDto> summary(@RequestHeader Map<String, String> h) {
|
||||
return ResponseEntity.ok(paperTradingService.getSummary(userId(h), deviceId(h), null));
|
||||
}
|
||||
|
||||
@PostMapping("/summary")
|
||||
public ResponseEntity<PaperSummaryDto> summaryWithMarks(
|
||||
@RequestHeader Map<String, String> h,
|
||||
@RequestBody(required = false) Map<String, Double> markPrices) {
|
||||
return ResponseEntity.ok(paperTradingService.getSummary(userId(h), deviceId(h), markPrices));
|
||||
}
|
||||
|
||||
@GetMapping("/trades")
|
||||
public ResponseEntity<List<PaperTradeDto>> trades(@RequestHeader Map<String, String> h) {
|
||||
return ResponseEntity.ok(paperTradingService.listTrades(userId(h), deviceId(h)));
|
||||
}
|
||||
|
||||
@PostMapping("/orders")
|
||||
public ResponseEntity<PaperTradeDto> placeOrder(
|
||||
@RequestHeader Map<String, String> h,
|
||||
@RequestBody PaperOrderRequest body) {
|
||||
return ResponseEntity.ok(paperTradingService.placeOrder(userId(h), deviceId(h), body));
|
||||
}
|
||||
|
||||
@PostMapping("/reset")
|
||||
public ResponseEntity<PaperSummaryDto> reset(@RequestHeader Map<String, String> h) {
|
||||
return ResponseEntity.ok(paperTradingService.resetAccount(userId(h), deviceId(h)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.StrategyDto;
|
||||
import com.goldenchart.service.StrategyService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 투자전략 CRUD REST API.
|
||||
*
|
||||
* <p>헤더:
|
||||
* <ul>
|
||||
* <li>X-Device-Id — 비회원 기기 식별자 (필수)</li>
|
||||
* <li>X-User-Id — 회원 ID (선택)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/strategies")
|
||||
@RequiredArgsConstructor
|
||||
public class StrategyController {
|
||||
|
||||
private final StrategyService service;
|
||||
|
||||
/** 전략 목록 조회 */
|
||||
@GetMapping
|
||||
public ResponseEntity<List<StrategyDto>> list(
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
return ResponseEntity.ok(service.list(parseUserId(userIdHeader), deviceId));
|
||||
}
|
||||
|
||||
/** 단일 전략 조회 */
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<StrategyDto> get(@PathVariable Long id) {
|
||||
return service.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/** 전략 저장 (id 없으면 생성, 있으면 수정) */
|
||||
@PostMapping
|
||||
public ResponseEntity<StrategyDto> save(
|
||||
@RequestBody StrategyDto dto,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
return ResponseEntity.ok(service.save(dto, parseUserId(userIdHeader), deviceId));
|
||||
}
|
||||
|
||||
/** 전략 삭제 */
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||
service.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private Long parseUserId(String header) {
|
||||
if (header == null || header.isBlank()) return null;
|
||||
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.TradeSignalDto;
|
||||
import com.goldenchart.service.TradeSignalService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 매매 시그널 이력 REST API.
|
||||
*
|
||||
* <pre>
|
||||
* GET /api/trade-signals → 전체 이력
|
||||
* GET /api/trade-signals?market= → 종목별 이력
|
||||
* </pre>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/trade-signals")
|
||||
@RequiredArgsConstructor
|
||||
public class TradeSignalController {
|
||||
|
||||
private final TradeSignalService service;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<TradeSignalDto>> list(
|
||||
@RequestParam(required = false) String market,
|
||||
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
|
||||
Long userId = parseUserId(userIdHeader);
|
||||
List<TradeSignalDto> result = (market != null && !market.isBlank())
|
||||
? service.listByMarket(userId, deviceId, market)
|
||||
: service.list(userId, deviceId);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
private Long parseUserId(String h) {
|
||||
if (h == null || h.isBlank()) return null;
|
||||
try { return Long.parseLong(h); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.entity.GcWatchlist;
|
||||
import com.goldenchart.entity.GcHoldings;
|
||||
import com.goldenchart.repository.GcHoldingsRepository;
|
||||
import com.goldenchart.repository.GcWatchlistRepository;
|
||||
import com.goldenchart.service.WatchlistService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 관심종목 / 보유종목 REST API.
|
||||
*
|
||||
* <p>관심종목 추가/삭제 시 {@link WatchlistService} 가 실시간 전략 체크 대상과 자동 연동한다.
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class WatchlistController {
|
||||
|
||||
private final WatchlistService watchlistService;
|
||||
private final GcWatchlistRepository watchlistRepo;
|
||||
private final GcHoldingsRepository holdingsRepo;
|
||||
|
||||
private Long userId(Map<String, String> h) {
|
||||
String v = h.get("x-user-id"); return v != null ? Long.parseLong(v) : null;
|
||||
}
|
||||
private String deviceId(Map<String, String> h) {
|
||||
return h.getOrDefault("x-device-id", "anonymous");
|
||||
}
|
||||
|
||||
// ── 관심종목 ──────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/watchlist")
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<List<GcWatchlist>> getWatchlist(@RequestHeader Map<String, String> h) {
|
||||
return ResponseEntity.ok(watchlistService.list(userId(h), deviceId(h)));
|
||||
}
|
||||
|
||||
@PostMapping("/watchlist")
|
||||
public ResponseEntity<GcWatchlist> addWatchlist(
|
||||
@RequestHeader Map<String, String> h,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
Long uid = userId(h);
|
||||
String symbol = (String) body.get("symbol");
|
||||
GcWatchlist item = watchlistService.add(
|
||||
uid,
|
||||
deviceId(h),
|
||||
symbol,
|
||||
(String) body.get("koreanName"),
|
||||
(String) body.get("englishName"),
|
||||
body.containsKey("displayOrder") ? (Integer) body.get("displayOrder") : null
|
||||
);
|
||||
return ResponseEntity.ok(item);
|
||||
}
|
||||
|
||||
@DeleteMapping("/watchlist/{symbol}")
|
||||
public ResponseEntity<Void> removeWatchlist(
|
||||
@RequestHeader Map<String, String> h,
|
||||
@PathVariable String symbol) {
|
||||
watchlistService.remove(userId(h), deviceId(h), symbol);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping("/watchlist/order")
|
||||
public ResponseEntity<Void> reorderWatchlist(
|
||||
@RequestHeader Map<String, String> h,
|
||||
@RequestBody List<Map<String, Object>> items) {
|
||||
items.forEach(item -> {
|
||||
String symbol = (String) item.get("symbol");
|
||||
int order = (Integer) item.get("displayOrder");
|
||||
Long uid = userId(h);
|
||||
watchlistService.list(uid, deviceId(h)).stream()
|
||||
.filter(w -> w.getSymbol().equals(symbol))
|
||||
.findFirst()
|
||||
.ifPresent(w -> { w.setDisplayOrder(order); watchlistRepo.save(w); });
|
||||
});
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── 보유종목 ──────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/holdings")
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<List<GcHoldings>> getHoldings(@RequestHeader Map<String, String> h) {
|
||||
Long uid = userId(h);
|
||||
return ResponseEntity.ok(uid != null
|
||||
? holdingsRepo.findByUserIdOrderByDisplayOrderAsc(uid)
|
||||
: holdingsRepo.findByDeviceIdOrderByDisplayOrderAsc(deviceId(h)));
|
||||
}
|
||||
|
||||
@PostMapping("/holdings")
|
||||
public ResponseEntity<GcHoldings> addHoldings(
|
||||
@RequestHeader Map<String, String> h,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
Long uid = userId(h);
|
||||
String did = uid == null ? deviceId(h) : null;
|
||||
String symbol = (String) body.get("symbol");
|
||||
|
||||
GcHoldings item = (uid != null
|
||||
? holdingsRepo.findByUserIdOrderByDisplayOrderAsc(uid)
|
||||
: holdingsRepo.findByDeviceIdOrderByDisplayOrderAsc(deviceId(h)))
|
||||
.stream().filter(hld -> hld.getSymbol().equals(symbol)).findFirst()
|
||||
.orElseGet(() -> GcHoldings.builder().userId(uid).deviceId(did).symbol(symbol).build());
|
||||
|
||||
item.setKoreanName((String) body.get("koreanName"));
|
||||
item.setEnglishName((String) body.get("englishName"));
|
||||
if (body.containsKey("avgPrice"))
|
||||
item.setAvgPrice(new BigDecimal(body.get("avgPrice").toString()));
|
||||
if (body.containsKey("quantity"))
|
||||
item.setQuantity(new BigDecimal(body.get("quantity").toString()));
|
||||
if (body.containsKey("displayOrder"))
|
||||
item.setDisplayOrder((Integer) body.get("displayOrder"));
|
||||
|
||||
return ResponseEntity.ok(holdingsRepo.save(item));
|
||||
}
|
||||
|
||||
@DeleteMapping("/holdings/{symbol}")
|
||||
public ResponseEntity<Void> removeHoldings(
|
||||
@RequestHeader Map<String, String> h,
|
||||
@PathVariable String symbol) {
|
||||
Long uid = userId(h);
|
||||
if (uid != null) holdingsRepo.deleteByUserIdAndSymbol(uid, symbol);
|
||||
else holdingsRepo.deleteByDeviceIdAndSymbol(deviceId(h), symbol);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AdminVerifyRequest {
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Ta4j AnalysisCriterion 계산 결과 전체를 담는 DTO
|
||||
*/
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public class BacktestAnalysisDto {
|
||||
|
||||
// ── 기본 자본 ─────────────────────────────────────────────────────────────
|
||||
double initialCapital;
|
||||
double finalEquity;
|
||||
|
||||
// ── 수익성 지표 ──────────────────────────────────────────────────────────
|
||||
/** 총 수익률 (소수, e.g. 0.152 = +15.2%) */
|
||||
double totalReturnPct;
|
||||
/** 총 손익 (금액) */
|
||||
double totalProfitLoss;
|
||||
/** 총 수익 (이익 거래 합산) */
|
||||
double grossProfit;
|
||||
/** 총 손실 (손실 거래 합산, 음수) */
|
||||
double grossLoss;
|
||||
/** 평균 거래 수익률 */
|
||||
double avgReturnPct;
|
||||
/** 손익비 (총이익 / |총손실|) */
|
||||
double profitLossRatio;
|
||||
|
||||
// ── 거래 통계 ─────────────────────────────────────────────────────────────
|
||||
int numberOfPositions;
|
||||
int numberOfWinning;
|
||||
int numberOfLosing;
|
||||
int numberOfBreakEven;
|
||||
/** 승률 0~1 */
|
||||
double winRate;
|
||||
|
||||
// ── 리스크 지표 ───────────────────────────────────────────────────────────
|
||||
/** 최대 낙폭 (음수, -0.15 = -15%) */
|
||||
double maxDrawdownPct;
|
||||
/** 최대 상승폭 */
|
||||
double maxRunupPct;
|
||||
/** 샤프 비율 */
|
||||
double sharpeRatio;
|
||||
/** 소르티노 비율 */
|
||||
double sortinoRatio;
|
||||
/** 칼마 비율 (총수익률 / |최대낙폭|) */
|
||||
double calmarRatio;
|
||||
/** Value at Risk (95%) */
|
||||
double valueAtRisk95;
|
||||
/** Expected Shortfall / CVaR */
|
||||
double expectedShortfall;
|
||||
|
||||
// ── 벤치마크 비교 ─────────────────────────────────────────────────────────
|
||||
/** 바이앤홀드 수익률 */
|
||||
double buyAndHoldReturnPct;
|
||||
/** 전략 대비 바이앤홀드 배수 (>1 이면 전략이 유리) */
|
||||
double vsBuyAndHold;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 백테스팅 요청 DTO.
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "strategyId": 1, // DB 전략 ID (선택 — 없으면 dsl 직접 전송)
|
||||
* "buyCondition": { ...LogicNode }, // 매수 조건 DSL
|
||||
* "sellCondition": { ...LogicNode }, // 매도 조건 DSL
|
||||
* "bars": [ {time, open, high, low, close, volume}, ... ],
|
||||
* "timeframe": "1D",
|
||||
* "indicatorParams": { "CCI": {"length":13}, ... } // 사용자 설정 파라미터 (선택)
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public class BacktestRequest {
|
||||
|
||||
/** DB 저장 전략 ID (선택 — 설정 시 buyCondition/sellCondition 무시) */
|
||||
private Long strategyId;
|
||||
|
||||
/** 매수 조건 DSL */
|
||||
private JsonNode buyCondition;
|
||||
|
||||
/** 매도 조건 DSL */
|
||||
private JsonNode sellCondition;
|
||||
|
||||
/** OHLCV 캔들 데이터 */
|
||||
private List<OhlcvBar> bars;
|
||||
|
||||
/** 타임프레임 (1m/5m/15m/30m/1h/4h/1D/1W/1M) */
|
||||
private String timeframe;
|
||||
|
||||
/**
|
||||
* 지표 파라미터 오버라이드.
|
||||
* key = indicatorType (e.g. "CCI"), value = {length:13, ...}
|
||||
* 없으면 IndicatorService 기본값 사용.
|
||||
*/
|
||||
private Map<String, Map<String, Object>> indicatorParams;
|
||||
|
||||
/** 백테스팅 설정 옵션 (선택 — 없으면 기본값 사용) */
|
||||
private BacktestSettingsDto settings;
|
||||
|
||||
/** 종목 코드 (결과 저장용, e.g. "KRW-BTC") */
|
||||
private String symbol;
|
||||
|
||||
/** 전략 이름 (결과 저장용) */
|
||||
private String strategyName;
|
||||
|
||||
/** 디바이스 ID (결과 저장용) */
|
||||
private String deviceId;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 백테스팅 결과 응답 DTO.
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "signals": [
|
||||
* { "time": 1700000000, "type": "BUY", "price": 42000 },
|
||||
* { "time": 1700200000, "type": "SELL", "price": 45000 }
|
||||
* ],
|
||||
* "stats": { "totalTrades": 8, "winRate": 0.625, ... }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public class BacktestResponse {
|
||||
|
||||
private List<Signal> signals;
|
||||
private Stats stats;
|
||||
/** Ta4j AnalysisCriterion 전체 결과 */
|
||||
private BacktestAnalysisDto analysis;
|
||||
/** DB 저장 후 부여된 결과 ID */
|
||||
private Long resultId;
|
||||
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public static class Signal {
|
||||
/** Unix timestamp (초) */
|
||||
private long time;
|
||||
/** BUY | SELL */
|
||||
private String type;
|
||||
/** 해당 봉 종가 */
|
||||
private double price;
|
||||
/** 진입/청산 인덱스 (0-based) */
|
||||
private int barIndex;
|
||||
}
|
||||
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public static class Stats {
|
||||
private int totalSignals;
|
||||
private int buySignals;
|
||||
private int sellSignals;
|
||||
/** 거래 쌍(매수→매도) 수 */
|
||||
private int totalTrades;
|
||||
/** 수익 거래 수 */
|
||||
private int winTrades;
|
||||
/** 승률 0~1 */
|
||||
private double winRate;
|
||||
/** 총 수익률 (소수, e.g. 0.15 = +15%) */
|
||||
private double totalReturn;
|
||||
/** 최대 낙폭 (소수, 음수, e.g. -0.12 = -12%) */
|
||||
private double maxDrawdown;
|
||||
/** 평균 수익률 per trade */
|
||||
private double avgReturn;
|
||||
/** 최종 자산 (초기 자본 × 복리 수익) */
|
||||
private double finalEquity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 백테스팅 설정 DTO.
|
||||
* Ta4j 의 비용 모델·손절/익절·트레일링 스탑·진입가격·재진입 등 모든 옵션을 담는다.
|
||||
*/
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public class BacktestSettingsDto {
|
||||
|
||||
private Long id;
|
||||
|
||||
// ── 자본 설정 ──────────────────────────────────────────────────────────────
|
||||
/** 초기 자본 (원) */
|
||||
@Builder.Default
|
||||
private BigDecimal initialCapital = new BigDecimal("10000000.00");
|
||||
|
||||
// ── 비용 모델 ──────────────────────────────────────────────────────────────
|
||||
/** LINEAR | ZERO */
|
||||
@Builder.Default
|
||||
private String commissionType = "LINEAR";
|
||||
|
||||
/** 수수료율 (예: 0.0015 = 0.15%) */
|
||||
@Builder.Default
|
||||
private BigDecimal commissionRate = new BigDecimal("0.00150");
|
||||
|
||||
/** 슬리피지율 (예: 0.0005 = 0.05%) */
|
||||
@Builder.Default
|
||||
private BigDecimal slippageRate = new BigDecimal("0.00050");
|
||||
|
||||
// ── 진입/청산 가격 ─────────────────────────────────────────────────────────
|
||||
/** CLOSE | NEXT_OPEN */
|
||||
@Builder.Default
|
||||
private String entryPriceType = "CLOSE";
|
||||
|
||||
/** CLOSE | NEXT_OPEN */
|
||||
@Builder.Default
|
||||
private String exitPriceType = "CLOSE";
|
||||
|
||||
// ── 포지션 방향 ────────────────────────────────────────────────────────────
|
||||
/** LONG | SHORT | BOTH */
|
||||
@Builder.Default
|
||||
private String positionDirection = "LONG";
|
||||
|
||||
// ── 거래 규모 ──────────────────────────────────────────────────────────────
|
||||
/** CAPITAL_PCT | FIXED_AMOUNT */
|
||||
@Builder.Default
|
||||
private String tradeSizeType = "CAPITAL_PCT";
|
||||
|
||||
/** 비율(%) 또는 고정 금액(원) */
|
||||
@Builder.Default
|
||||
private BigDecimal tradeSizeValue = new BigDecimal("100.0000");
|
||||
|
||||
// ── 손절 (StopLossRule) ───────────────────────────────────────────────────
|
||||
@Builder.Default
|
||||
private Boolean stopLossEnabled = false;
|
||||
|
||||
/** 손절 비율 (%) */
|
||||
@Builder.Default
|
||||
private BigDecimal stopLossPct = new BigDecimal("2.000");
|
||||
|
||||
// ── 익절 (StopGainRule) ───────────────────────────────────────────────────
|
||||
@Builder.Default
|
||||
private Boolean takeProfitEnabled = false;
|
||||
|
||||
/** 익절 비율 (%) */
|
||||
@Builder.Default
|
||||
private BigDecimal takeProfitPct = new BigDecimal("5.000");
|
||||
|
||||
// ── 트레일링 스탑 (TrailingStopLossRule) ─────────────────────────────────
|
||||
@Builder.Default
|
||||
private Boolean trailingStopEnabled = false;
|
||||
|
||||
/** 트레일링 스탑 비율 (%) */
|
||||
@Builder.Default
|
||||
private BigDecimal trailingStopPct = new BigDecimal("2.000");
|
||||
|
||||
// ── 재진입 제어 ────────────────────────────────────────────────────────────
|
||||
/** 매도 후 재진입 대기 봉 수 */
|
||||
@Builder.Default
|
||||
private Integer reentryWaitBars = 0;
|
||||
|
||||
/** 최대 동시 보유 포지션 수 */
|
||||
@Builder.Default
|
||||
private Integer maxOpenTrades = 1;
|
||||
|
||||
// ── 포지션 종속성 모드 ─────────────────────────────────────────────────────
|
||||
/** "LONG_ONLY" | "SIGNAL_ONLY" — 매도 시그널 포지션 종속성 제어 */
|
||||
@Builder.Default
|
||||
private String positionMode = "LONG_ONLY";
|
||||
|
||||
// ── 분할 청산 ──────────────────────────────────────────────────────────────
|
||||
@Builder.Default
|
||||
private Boolean partialExitEnabled = false;
|
||||
|
||||
/** 분할 청산 비율 (%) */
|
||||
@Builder.Default
|
||||
private BigDecimal partialExitPct = new BigDecimal("50.000");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* REST API / STOMP 용 캔들 데이터 전송 객체.
|
||||
*
|
||||
* REST : GET /api/candles/history 응답 배열 요소
|
||||
* STOMP : /sub/charts/{market}/{type} 실시간 경량 페이로드
|
||||
*
|
||||
* 명세서 5.1 / 5.2 포맷 준수:
|
||||
* - time : Unix 초 타임스탬프
|
||||
* - o/h/l/c/v : OHLCV
|
||||
* - rsi : RSI 값 (null 허용)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CandleBarDto {
|
||||
|
||||
/** Unix 초 타임스탬프 (캔들 시작 시간) */
|
||||
private long time;
|
||||
|
||||
private double open;
|
||||
private double high;
|
||||
private double low;
|
||||
private double close;
|
||||
private double volume;
|
||||
|
||||
/** RSI 값. 계산 불가 구간(워밍업)에서는 null */
|
||||
private Double rsi;
|
||||
|
||||
/**
|
||||
* 실시간 전략 체크 시그널.
|
||||
* "BUY" | "SELL" | "NONE" (null 이면 전략 체크 미설정)
|
||||
*/
|
||||
private String signal;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CreateUserRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
private String displayName;
|
||||
private String role;
|
||||
private Boolean enabled;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FcmTokenDto {
|
||||
private String token;
|
||||
private Long userId;
|
||||
private String deviceId;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class IndicatorRequest {
|
||||
private String symbol;
|
||||
private String timeframe;
|
||||
/** 지표 타입 (RSI, MACD, BollingerBands ...) */
|
||||
private String indicatorType;
|
||||
/** 지표 파라미터 */
|
||||
private Map<String, Object> params;
|
||||
/** 직접 캔들 데이터 전달 (선택: 없으면 DB에서 조회) */
|
||||
private List<OhlcvBar> bars;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 지표 계산 결과 응답.
|
||||
* frontend PlotData 구조와 1:1 매핑.
|
||||
*/
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public class IndicatorResponse {
|
||||
private String symbol;
|
||||
private String timeframe;
|
||||
private String indicatorType;
|
||||
/** time 배열 (Unix 초) */
|
||||
private List<Long> times;
|
||||
/**
|
||||
* plot ID → [{time, value, color?}] 배열.
|
||||
* e.g. MACD: {"histogram":[...], "macd":[...], "signal":[...]}
|
||||
*/
|
||||
private Map<String, List<PlotPoint>> values;
|
||||
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public static class PlotPoint {
|
||||
private long time;
|
||||
private Double value;
|
||||
private String color;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LiveOrderRequest {
|
||||
private String market;
|
||||
/** BUY | SELL */
|
||||
private String side;
|
||||
/** market | limit — 현재 market 만 지원 */
|
||||
private String orderKind;
|
||||
/** 시장가 매수 시 KRW 금액 (미지정 시 예산 % 적용) */
|
||||
private Double krwAmount;
|
||||
/** 시장가 매도 시 수량 (미지정 시 전량) */
|
||||
private Double quantity;
|
||||
private Long strategyId;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 관심종목 등 여러 마켓에 동일한 실시간 전략 설정을 일괄 적용 */
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LiveStrategyBulkRequest {
|
||||
|
||||
/** 적용할 마켓 코드 목록 (e.g. KRW-BTC) */
|
||||
private List<String> markets;
|
||||
|
||||
/** 각 마켓에 복사할 설정 (market 필드는 서버에서 markets 항목으로 덮어씀) */
|
||||
private LiveStrategySettingsDto template;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 실시간 전략 체크 설정 DTO.
|
||||
* PUT /api/strategy/settings (요청/응답 공용)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LiveStrategySettingsDto {
|
||||
|
||||
/** 업비트 마켓 코드 (e.g. "KRW-BTC") */
|
||||
private String market;
|
||||
|
||||
/** 연결된 전략 ID (null = 미연결) */
|
||||
private Long strategyId;
|
||||
|
||||
/** 실시간 체크 ON/OFF (JSON 키: isLiveCheck) */
|
||||
@JsonProperty("isLiveCheck")
|
||||
private boolean liveCheck;
|
||||
|
||||
/** "CANDLE_CLOSE" | "REALTIME_TICK" */
|
||||
private String executionType;
|
||||
|
||||
/** 전략 평가 분봉: 1m, 3m, 5m, 15m, 30m, 1h, 4h, 1d */
|
||||
@Builder.Default
|
||||
private String candleType = "1m";
|
||||
|
||||
/** 매도 시그널 포지션 종속성 모드: "LONG_ONLY" | "SIGNAL_ONLY" */
|
||||
@Builder.Default
|
||||
private String positionMode = "LONG_ONLY";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LiveSummaryDto {
|
||||
private boolean enabled;
|
||||
private boolean configured;
|
||||
private String tradingMode;
|
||||
private double krwBalance;
|
||||
private List<Position> positions;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Position {
|
||||
private String symbol;
|
||||
private double quantity;
|
||||
private double avgPrice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LiveTradeDto {
|
||||
private Long id;
|
||||
private String symbol;
|
||||
private String side;
|
||||
private String source;
|
||||
private double price;
|
||||
private double quantity;
|
||||
private double grossAmount;
|
||||
private String upbitOrderUuid;
|
||||
private String createdAt;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String displayName;
|
||||
/** ADMIN | USER | GUEST */
|
||||
private String role;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MenuPermissionsResponse {
|
||||
private String role;
|
||||
private Map<String, Boolean> permissions;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* OHLCV 캔들 데이터 DTO.
|
||||
* frontend OHLCVBar 와 동일 구조.
|
||||
*/
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public class OhlcvBar {
|
||||
/** Unix timestamp (초) */
|
||||
private long time;
|
||||
private double open;
|
||||
private double high;
|
||||
private double low;
|
||||
private double close;
|
||||
private double volume;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PaperOrderRequest {
|
||||
private String market;
|
||||
private String side; // BUY | SELL
|
||||
private String orderKind; // limit | market
|
||||
private Double price;
|
||||
private Double quantity;
|
||||
/** 수량 미입력 시 매수 예산 비율 (가용현금 %) — 기본값: 설정의 paperAutoTradeBudgetPct */
|
||||
private Double budgetPct;
|
||||
/** 수량 미입력 시 매수 고정 금액 (KRW) — budgetPct보다 우선 */
|
||||
private Double krwAmount;
|
||||
private String source; // MANUAL | STRATEGY (optional)
|
||||
private Long strategyId;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PaperPositionDto {
|
||||
private Long id;
|
||||
private String symbol;
|
||||
private String koreanName;
|
||||
private Double quantity;
|
||||
private Double avgPrice;
|
||||
/** 프론트가 현재가를 넣어 평가손익 계산 시 사용 (선택) */
|
||||
private Double markPrice;
|
||||
private Double evalAmount;
|
||||
private Double profitLoss;
|
||||
private Double profitLossPct;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PaperSummaryDto {
|
||||
private Boolean enabled;
|
||||
private Double initialCapital;
|
||||
private Double cashBalance;
|
||||
private Double stockEvalAmount;
|
||||
private Double totalAsset;
|
||||
private Double unrealizedPnl;
|
||||
private Double realizedPnl;
|
||||
private Double totalReturnPct;
|
||||
private Double feeRatePct;
|
||||
private Double slippagePct;
|
||||
private Double minOrderKrw;
|
||||
private Boolean autoTradeEnabled;
|
||||
private Double autoTradeBudgetPct;
|
||||
private List<PaperPositionDto> positions;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PaperTradeDto {
|
||||
private Long id;
|
||||
private String symbol;
|
||||
private String side;
|
||||
private String orderKind;
|
||||
private String source;
|
||||
private Long strategyId;
|
||||
private Double price;
|
||||
private Double quantity;
|
||||
private Double grossAmount;
|
||||
private Double feeAmount;
|
||||
private Double netAmount;
|
||||
private Double cashAfter;
|
||||
private String createdAt;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class RolePermissionsUpdateRequest {
|
||||
private String role;
|
||||
private Map<String, Boolean> permissions;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonRawValue;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 투자전략 CRUD DTO.
|
||||
* frontend StrategyDto 와 1:1 대응.
|
||||
*/
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
public class StrategyDto {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
|
||||
/** 매수 조건 LogicNode 트리 (JSON 그대로 직렬화) */
|
||||
private JsonNode buyCondition;
|
||||
|
||||
/** 매도 조건 LogicNode 트리 (JSON 그대로 직렬화) */
|
||||
private JsonNode sellCondition;
|
||||
|
||||
private Boolean enabled;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 매매 시그널 이력 DTO.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TradeSignalDto {
|
||||
private Long id;
|
||||
private String market;
|
||||
private Long strategyId;
|
||||
private String strategyName;
|
||||
/** BUY | SELL */
|
||||
private String signalType;
|
||||
private Double price;
|
||||
private Long candleTime;
|
||||
private String candleType;
|
||||
/** CANDLE_CLOSE | REALTIME_TICK */
|
||||
private String executionType;
|
||||
private String createdAt;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder
|
||||
public record UpbitApiCredentials(String accessKey, String secretKey) {
|
||||
public boolean isComplete() {
|
||||
return accessKey != null && !accessKey.isBlank()
|
||||
&& secretKey != null && !secretKey.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UpdateUserRequest {
|
||||
private String password;
|
||||
private String displayName;
|
||||
private String role;
|
||||
private Boolean enabled;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String displayName;
|
||||
private String role;
|
||||
private Boolean enabled;
|
||||
private String createdAt;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 앱 전역 차트 기본 설정.
|
||||
*
|
||||
* <p>프론트엔드 코드에 하드코딩된 다음 값들을 DB 로 대체한다:
|
||||
* <ul>
|
||||
* <li>기본 심볼 (DEFAULT_STATE.symbol → 'KRW-BTC')</li>
|
||||
* <li>기본 타임프레임 (DEFAULT_STATE.timeframe → '1D')</li>
|
||||
* <li>기본 차트 타입 ('candlestick')</li>
|
||||
* <li>기본 테마 ('dark')</li>
|
||||
* <li>기본 로그 스케일 (false)</li>
|
||||
* <li>기본 레이아웃 ID ('1')</li>
|
||||
* <li>캔들 색상 (DEFAULT_MAIN_CHART_STYLE)</li>
|
||||
* <li>멀티차트 동기화 옵션 (DEFAULT_SYNC)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_app_settings")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcAppSettings {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "device_id", length = 100, unique = true)
|
||||
private String deviceId;
|
||||
|
||||
@Column(name = "user_id", unique = true)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "default_symbol", length = 50, nullable = false)
|
||||
@Builder.Default
|
||||
private String defaultSymbol = "KRW-BTC";
|
||||
|
||||
@Column(name = "default_timeframe", length = 10, nullable = false)
|
||||
@Builder.Default
|
||||
private String defaultTimeframe = "1D";
|
||||
|
||||
@Column(name = "default_chart_type", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String defaultChartType = "candlestick";
|
||||
|
||||
@Column(name = "default_theme", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String defaultTheme = "dark";
|
||||
|
||||
@Column(name = "default_log_scale", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean defaultLogScale = false;
|
||||
|
||||
@Column(name = "default_layout_id", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String defaultLayoutId = "1";
|
||||
|
||||
/** 차트·UI 시간 표시 IANA 시간대 */
|
||||
@Column(name = "display_timezone", length = 64, nullable = false)
|
||||
@Builder.Default
|
||||
private String displayTimezone = "Asia/Seoul";
|
||||
|
||||
/** 캔들 색상 JSON (frontend MainChartStyle 구조) */
|
||||
@Column(name = "main_chart_style_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String mainChartStyleJson;
|
||||
|
||||
/** 멀티차트 동기화 옵션 JSON (frontend SyncOptions 구조) */
|
||||
@Column(name = "sync_options_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String syncOptionsJson;
|
||||
|
||||
/** 백테스팅 완료 시 결과 팝업 자동 표시 여부 (기본 true) */
|
||||
@Column(name = "bt_auto_popup", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean btAutoPopup = true;
|
||||
|
||||
/** 백테스팅 매수/매도 마커에 금액 표시 여부 (기본 true) */
|
||||
@Column(name = "bt_show_price", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean btShowPrice = true;
|
||||
|
||||
/** 보조지표 우측 가격축 라벨·설명·금액 하이라이트 (기본 true) */
|
||||
@Column(name = "chart_series_price_labels", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean chartSeriesPriceLabels = true;
|
||||
|
||||
/** 차트 하단 거래량 바 표시 (기본 true) */
|
||||
@Column(name = "chart_volume_visible", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean chartVolumeVisible = true;
|
||||
|
||||
/** 차트 상단 범례(tv-legend) 항목별 표시 옵션 JSON */
|
||||
@Column(name = "chart_legend_options_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String chartLegendOptionsJson;
|
||||
|
||||
/** 매매 시그널 발생 시 알림 팝업 표시 여부 (기본 true) */
|
||||
@Column(name = "trade_alert_popup", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean tradeAlertPopup = true;
|
||||
|
||||
/** 매매 시그널 알림 사운드 재생 여부 */
|
||||
@Column(name = "trade_alert_sound_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean tradeAlertSoundEnabled = true;
|
||||
|
||||
/** 매매 시그널 알림음 ID (bell, chime, silent 등) */
|
||||
@Column(name = "trade_alert_sound", nullable = false, length = 32)
|
||||
@Builder.Default
|
||||
private String tradeAlertSound = "bell";
|
||||
|
||||
/** 알림 팝업 위치: right | left | bottom */
|
||||
@Column(name = "trade_alert_popup_position", nullable = false, length = 10)
|
||||
@Builder.Default
|
||||
private String tradeAlertPopupPosition = "right";
|
||||
|
||||
/** 알림 팝업 배치: stack | grid | strip | single */
|
||||
@Column(name = "trade_alert_popup_layout", nullable = false, length = 10)
|
||||
@Builder.Default
|
||||
private String tradeAlertPopupLayout = "stack";
|
||||
|
||||
/** 그리드 배치 시 열 개수 (2~4) */
|
||||
@Column(name = "trade_alert_popup_grid_cols", nullable = false)
|
||||
@Builder.Default
|
||||
private Integer tradeAlertPopupGridCols = 2;
|
||||
|
||||
/** 실시간 전략 체크 마스터 ON/OFF — ON 이면 DB 관심종목 전체가 체크 대상 */
|
||||
@Column(name = "live_strategy_check", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean liveStrategyCheck = false;
|
||||
|
||||
/** 관심종목에 공통 적용할 전략 ID */
|
||||
@Column(name = "live_strategy_id")
|
||||
private Long liveStrategyId;
|
||||
|
||||
@Column(name = "live_execution_type", nullable = false, length = 30)
|
||||
@Builder.Default
|
||||
private String liveExecutionType = "CANDLE_CLOSE";
|
||||
|
||||
@Column(name = "live_position_mode", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String livePositionMode = "LONG_ONLY";
|
||||
|
||||
/** 모의투자 마스터 ON/OFF */
|
||||
@Column(name = "paper_trading_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean paperTradingEnabled = true;
|
||||
|
||||
@Column(name = "paper_initial_capital", nullable = false, precision = 20, scale = 2)
|
||||
@Builder.Default
|
||||
private BigDecimal paperInitialCapital = BigDecimal.valueOf(10_000_000);
|
||||
|
||||
@Column(name = "paper_fee_rate_pct", nullable = false, precision = 8, scale = 4)
|
||||
@Builder.Default
|
||||
private BigDecimal paperFeeRatePct = BigDecimal.valueOf(0.05);
|
||||
|
||||
@Column(name = "paper_slippage_pct", nullable = false, precision = 8, scale = 4)
|
||||
@Builder.Default
|
||||
private BigDecimal paperSlippagePct = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "paper_min_order_krw", nullable = false, precision = 20, scale = 2)
|
||||
@Builder.Default
|
||||
private BigDecimal paperMinOrderKrw = BigDecimal.valueOf(5000);
|
||||
|
||||
/** 실시간 전략 시그널 시 자동 모의매매 */
|
||||
@Column(name = "paper_auto_trade_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean paperAutoTradeEnabled = false;
|
||||
|
||||
@Column(name = "paper_auto_trade_budget_pct", nullable = false, precision = 8, scale = 4)
|
||||
@Builder.Default
|
||||
private BigDecimal paperAutoTradeBudgetPct = BigDecimal.valueOf(95);
|
||||
|
||||
/** PAPER | LIVE | BOTH — 자동매매 실행 대상 */
|
||||
@Column(name = "trading_mode", nullable = false, length = 10)
|
||||
@Builder.Default
|
||||
private String tradingMode = "PAPER";
|
||||
|
||||
/** 실거래(Upbit API) 자동매매 ON */
|
||||
@Column(name = "live_auto_trade_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean liveAutoTradeEnabled = false;
|
||||
|
||||
@Column(name = "upbit_access_key", length = 128)
|
||||
private String upbitAccessKey;
|
||||
|
||||
@Column(name = "upbit_secret_key", length = 256)
|
||||
private String upbitSecretKey;
|
||||
|
||||
/** BACKEND_STOMP | UPBIT_DIRECT */
|
||||
@Column(name = "chart_realtime_source", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String chartRealtimeSource = "BACKEND_STOMP";
|
||||
|
||||
@Column(name = "live_auto_trade_budget_pct", nullable = false, precision = 8, scale = 4)
|
||||
@Builder.Default
|
||||
private BigDecimal liveAutoTradeBudgetPct = BigDecimal.valueOf(95);
|
||||
|
||||
@Column(name = "fcm_push_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean fcmPushEnabled = false;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gc_backtest_result")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcBacktestResult {
|
||||
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "device_id", length = 100) private String deviceId;
|
||||
@Column(name = "strategy_id") private Long strategyId;
|
||||
@Column(name = "strategy_name", length = 200) private String strategyName;
|
||||
@Column(name = "symbol", length = 50) private String symbol;
|
||||
@Column(name = "timeframe", length = 10) private String timeframe;
|
||||
@Column(name = "bar_count") private Integer barCount;
|
||||
@Column(name = "from_time") private Long fromTime;
|
||||
@Column(name = "to_time") private Long toTime;
|
||||
|
||||
@Column(name = "settings_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON) private String settingsJson;
|
||||
|
||||
@Column(name = "signals_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON) private String signalsJson;
|
||||
|
||||
@Column(name = "analysis_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON) private String analysisJson;
|
||||
|
||||
@Column(name = "total_return", precision = 12, scale = 4) private BigDecimal totalReturn;
|
||||
@Column(name = "win_rate", precision = 6, scale = 4) private BigDecimal winRate;
|
||||
@Column(name = "total_trades") private Integer totalTrades;
|
||||
@Column(name = "max_drawdown", precision = 12, scale = 4) private BigDecimal maxDrawdown;
|
||||
@Column(name = "sharpe_ratio", precision = 10, scale = 4) private BigDecimal sharpeRatio;
|
||||
@Column(name = "final_equity", precision = 20, scale = 2) private BigDecimal finalEquity;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); }
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 백테스팅 설정 엔티티.
|
||||
* Ta4j 의 비용 모델·손절/익절/트레일링 스탑·진입가격·재진입 규칙 등을 저장.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_backtest_settings")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcBacktestSettings {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "device_id", length = 100)
|
||||
private String deviceId;
|
||||
|
||||
// ── 자본 설정 ──────────────────────────────────────────────────────────────
|
||||
@Column(name = "initial_capital", nullable = false, precision = 20, scale = 2)
|
||||
@Builder.Default
|
||||
private BigDecimal initialCapital = new BigDecimal("10000000.00");
|
||||
|
||||
// ── 비용 모델 ──────────────────────────────────────────────────────────────
|
||||
/** LINEAR | ZERO */
|
||||
@Column(name = "commission_type", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String commissionType = "LINEAR";
|
||||
|
||||
@Column(name = "commission_rate", nullable = false, precision = 8, scale = 5)
|
||||
@Builder.Default
|
||||
private BigDecimal commissionRate = new BigDecimal("0.00150");
|
||||
|
||||
@Column(name = "slippage_rate", nullable = false, precision = 8, scale = 5)
|
||||
@Builder.Default
|
||||
private BigDecimal slippageRate = new BigDecimal("0.00050");
|
||||
|
||||
// ── 진입/청산 가격 ─────────────────────────────────────────────────────────
|
||||
/** CLOSE | NEXT_OPEN */
|
||||
@Column(name = "entry_price_type", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String entryPriceType = "CLOSE";
|
||||
|
||||
/** CLOSE | NEXT_OPEN */
|
||||
@Column(name = "exit_price_type", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String exitPriceType = "CLOSE";
|
||||
|
||||
// ── 포지션 방향 ────────────────────────────────────────────────────────────
|
||||
/** LONG | SHORT | BOTH */
|
||||
@Column(name = "position_direction", nullable = false, length = 10)
|
||||
@Builder.Default
|
||||
private String positionDirection = "LONG";
|
||||
|
||||
// ── 거래 규모 ──────────────────────────────────────────────────────────────
|
||||
/** CAPITAL_PCT | FIXED_AMOUNT */
|
||||
@Column(name = "trade_size_type", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String tradeSizeType = "CAPITAL_PCT";
|
||||
|
||||
@Column(name = "trade_size_value", nullable = false, precision = 10, scale = 4)
|
||||
@Builder.Default
|
||||
private BigDecimal tradeSizeValue = new BigDecimal("100.0000");
|
||||
|
||||
// ── 손절 ───────────────────────────────────────────────────────────────────
|
||||
@Column(name = "stop_loss_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean stopLossEnabled = false;
|
||||
|
||||
@Column(name = "stop_loss_pct", nullable = false, precision = 6, scale = 3)
|
||||
@Builder.Default
|
||||
private BigDecimal stopLossPct = new BigDecimal("2.000");
|
||||
|
||||
// ── 익절 ───────────────────────────────────────────────────────────────────
|
||||
@Column(name = "take_profit_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean takeProfitEnabled = false;
|
||||
|
||||
@Column(name = "take_profit_pct", nullable = false, precision = 6, scale = 3)
|
||||
@Builder.Default
|
||||
private BigDecimal takeProfitPct = new BigDecimal("5.000");
|
||||
|
||||
// ── 트레일링 스탑 ──────────────────────────────────────────────────────────
|
||||
@Column(name = "trailing_stop_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean trailingStopEnabled = false;
|
||||
|
||||
@Column(name = "trailing_stop_pct", nullable = false, precision = 6, scale = 3)
|
||||
@Builder.Default
|
||||
private BigDecimal trailingStopPct = new BigDecimal("2.000");
|
||||
|
||||
// ── 재진입 제어 ────────────────────────────────────────────────────────────
|
||||
@Column(name = "reentry_wait_bars", nullable = false)
|
||||
@Builder.Default
|
||||
private Integer reentryWaitBars = 0;
|
||||
|
||||
@Column(name = "max_open_trades", nullable = false)
|
||||
@Builder.Default
|
||||
private Integer maxOpenTrades = 1;
|
||||
|
||||
// ── 포지션 종속성 모드 ─────────────────────────────────────────────────────
|
||||
/** LONG_ONLY | SIGNAL_ONLY */
|
||||
@Column(name = "position_mode", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String positionMode = "LONG_ONLY";
|
||||
|
||||
// ── 분할 청산 ──────────────────────────────────────────────────────────────
|
||||
@Column(name = "partial_exit_enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean partialExitEnabled = false;
|
||||
|
||||
@Column(name = "partial_exit_pct", nullable = false, precision = 6, scale = 3)
|
||||
@Builder.Default
|
||||
private BigDecimal partialExitPct = new BigDecimal("50.000");
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() { createdAt = updatedAt = LocalDateTime.now(); }
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() { updatedAt = LocalDateTime.now(); }
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 차트 슬롯 설정.
|
||||
* 멀티차트 레이아웃 내 각 슬롯의 모든 설정값을 저장.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_chart_slot",
|
||||
uniqueConstraints = @UniqueConstraint(name = "uk_slot_workspace_index",
|
||||
columnNames = {"workspace_id", "slot_index"}))
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcChartSlot {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "workspace_id", nullable = false)
|
||||
private GcChartWorkspace workspace;
|
||||
|
||||
/** 슬롯 인덱스 (0-based) */
|
||||
@Column(name = "slot_index", nullable = false)
|
||||
private Integer slotIndex;
|
||||
|
||||
/** 종목 코드 (e.g. "KRW-BTC", "AAPL") */
|
||||
@Column(name = "symbol", length = 50)
|
||||
private String symbol;
|
||||
|
||||
/** 타임프레임 (1m/5m/15m/30m/1h/4h/1D/1W/1M) */
|
||||
@Column(name = "timeframe", length = 10)
|
||||
private String timeframe;
|
||||
|
||||
/** 차트 타입 (candlestick/bar/line/area/baseline) */
|
||||
@Column(name = "chart_type", length = 20)
|
||||
@Builder.Default
|
||||
private String chartType = "candlestick";
|
||||
|
||||
/** 테마 (dark/light/blue) */
|
||||
@Column(name = "theme", length = 20)
|
||||
@Builder.Default
|
||||
private String theme = "dark";
|
||||
|
||||
/** 차트 모드 (chart/trading) */
|
||||
@Column(name = "mode", length = 20)
|
||||
@Builder.Default
|
||||
private String mode = "chart";
|
||||
|
||||
/** 로그 스케일 사용 여부 */
|
||||
@Column(name = "log_scale")
|
||||
@Builder.Default
|
||||
private Boolean logScale = false;
|
||||
|
||||
/**
|
||||
* 지표 설정 JSON 배열.
|
||||
* 형식: [{id, type, params, hidden, plotVisibility}]
|
||||
* frontend IndicatorConfig[] 구조와 1:1 대응
|
||||
*/
|
||||
@Column(name = "indicators_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String indicatorsJson;
|
||||
|
||||
/**
|
||||
* 드로잉 객체 JSON 배열.
|
||||
* 형식: [{id, type, points, color, lineWidth, style, text, visible, fibtzSettings}]
|
||||
* frontend Drawing[] 구조와 1:1 대응
|
||||
*/
|
||||
@Column(name = "drawings_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String drawingsJson;
|
||||
|
||||
/** 드로잉 잠금 여부 */
|
||||
@Column(name = "drawings_locked")
|
||||
@Builder.Default
|
||||
private Boolean drawingsLocked = false;
|
||||
|
||||
/** 드로잉 표시 여부 */
|
||||
@Column(name = "drawings_visible")
|
||||
@Builder.Default
|
||||
private Boolean drawingsVisible = true;
|
||||
|
||||
/**
|
||||
* 보조지표 창(pane) 레이아웃 JSON.
|
||||
* 형식: [{paneIndex, height}]
|
||||
*/
|
||||
@Column(name = "pane_layout_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String paneLayoutJson;
|
||||
|
||||
/**
|
||||
* 메인 차트 캔들 색상 설정 JSON.
|
||||
* frontend MainChartStyle 구조
|
||||
*/
|
||||
@Column(name = "main_chart_style_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String mainChartStyleJson;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 사용자 차트 워크스페이스.
|
||||
* 멀티차트 레이아웃 및 슬롯 집합을 관리한다.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_chart_workspace")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcChartWorkspace {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** 소유 사용자 ID (users.id 논리 참조) */
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
/** 비회원 기기 식별자 */
|
||||
@Column(name = "device_id", length = 100)
|
||||
private String deviceId;
|
||||
|
||||
/** 현재 적용된 레이아웃 ID (frontend layoutTypes.ts 의 LayoutDef.id 와 일치: "1","2v","2h",...) */
|
||||
@Column(name = "layout_id", length = 20, nullable = false)
|
||||
private String layoutId;
|
||||
|
||||
/** 차트 슬롯 설정 목록 (1:N) */
|
||||
@OneToMany(mappedBy = "workspace", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("slotIndex ASC")
|
||||
@Builder.Default
|
||||
private List<GcChartSlot> slots = new ArrayList<>();
|
||||
|
||||
/** 차트 간 동기화 옵션 JSON {"symbol":true,"timeframe":false,"crosshair":true} */
|
||||
@Column(name = "sync_options_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String syncOptionsJson;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gc_fcm_token")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class GcFcmToken {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "device_id", nullable = false, length = 64)
|
||||
private String deviceId;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 512)
|
||||
private String token;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean active = true;
|
||||
|
||||
@Column(name = "last_used_at")
|
||||
private LocalDateTime lastUsedAt;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 보유종목.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_holdings",
|
||||
uniqueConstraints = @UniqueConstraint(name = "uk_holdings_user_symbol",
|
||||
columnNames = {"user_id", "device_id", "symbol"}))
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcHoldings {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "device_id", length = 100)
|
||||
private String deviceId;
|
||||
|
||||
@Column(name = "symbol", length = 50, nullable = false)
|
||||
private String symbol;
|
||||
|
||||
@Column(name = "korean_name", length = 100)
|
||||
private String koreanName;
|
||||
|
||||
@Column(name = "english_name", length = 100)
|
||||
private String englishName;
|
||||
|
||||
/** 평균 매입가 */
|
||||
@Column(name = "avg_price", precision = 30, scale = 8)
|
||||
private BigDecimal avgPrice;
|
||||
|
||||
/** 보유 수량 */
|
||||
@Column(name = "quantity", precision = 30, scale = 8)
|
||||
private BigDecimal quantity;
|
||||
|
||||
@Column(name = "display_order")
|
||||
@Builder.Default
|
||||
private Integer displayOrder = 0;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 장치/사용자별 전역 지표 파라미터 설정.
|
||||
*
|
||||
* params_json 구조 예시:
|
||||
* {
|
||||
* "RSI": {"length": 9, "src": "close"},
|
||||
* "MACD": {"fastLength": 12, "slowLength": 26, "signalLength": 9, "src": "close"},
|
||||
* "BollingerBands": {"length": 20, "mult": 2.0, "src": "close"},
|
||||
* "Stochastic": {"kLength": 14, "smooth": 3, "dSmoothing": 3},
|
||||
* "IchimokuCloud": {"conversionPeriods": 9, "basePeriods": 26,
|
||||
* "laggingSpan2Periods": 52, "displacement": 26},
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* 프론트엔드 indicatorRegistry.ts 의 defaultParams 를 완전히 대체한다.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_indicator_settings")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcIndicatorSettings {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** 비회원 기기 식별자 (X-Device-Id 헤더) */
|
||||
@Column(name = "device_id", length = 100, unique = true)
|
||||
private String deviceId;
|
||||
|
||||
/** 회원 ID (향후 확장) */
|
||||
@Column(name = "user_id", unique = true)
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 지표 타입 → 파라미터 맵의 JSON 직렬화.
|
||||
* Map<String, Map<String, Object>> 형태로 역직렬화한다.
|
||||
*/
|
||||
@Column(name = "params_json", columnDefinition = "JSON", nullable = false)
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Builder.Default
|
||||
private String paramsJson = "{}";
|
||||
|
||||
/**
|
||||
* 지표 타입 → 시각 설정(색상·선굵기·수평선) JSON 직렬화.
|
||||
* Map<String, IndicatorVisual> 형태로 역직렬화한다.
|
||||
* <pre>
|
||||
* {
|
||||
* "RSI": {
|
||||
* "plots": [{"id":"plot0","color":"#7E57C2","lineWidth":2,"type":"line"}],
|
||||
* "hlines": [{"price":70,"color":"#EF5350","visible":true}]
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@Column(name = "visual_config_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Builder.Default
|
||||
private String visualConfigJson = "{}";
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 디바이스/유저 × 마켓별 실시간 전략 체크 설정.
|
||||
*
|
||||
* <ul>
|
||||
* <li>isLiveCheck — 실시간 체크 ON/OFF</li>
|
||||
* <li>executionType — CANDLE_CLOSE | REALTIME_TICK</li>
|
||||
* <li>strategyId — 연결된 gc_strategy.id (null 이면 체크 비활성)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_live_strategy_settings")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcLiveStrategySettings {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "device_id", length = 100)
|
||||
private String deviceId;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "market", nullable = false, length = 30)
|
||||
@Builder.Default
|
||||
private String market = "KRW-BTC";
|
||||
|
||||
/** 연결된 전략 ID (null = 미연결) */
|
||||
@Column(name = "strategy_id")
|
||||
private Long strategyId;
|
||||
|
||||
@Column(name = "is_live_check", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean isLiveCheck = false;
|
||||
|
||||
/** CANDLE_CLOSE | REALTIME_TICK */
|
||||
@Column(name = "execution_type", nullable = false, length = 30)
|
||||
@Builder.Default
|
||||
private String executionType = "CANDLE_CLOSE";
|
||||
|
||||
/** 전략 평가·데이터 수집 분봉 (1m, 3m, 5m, …) */
|
||||
@Column(name = "candle_type", nullable = false, length = 10)
|
||||
@Builder.Default
|
||||
private String candleType = "1m";
|
||||
|
||||
/** 매도 시그널 포지션 종속성 모드: LONG_ONLY | SIGNAL_ONLY */
|
||||
@Column(name = "position_mode", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String positionMode = "LONG_ONLY";
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gc_live_trade")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcLiveTrade {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "device_id", nullable = false, length = 100)
|
||||
private String deviceId;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "symbol", nullable = false, length = 30)
|
||||
private String symbol;
|
||||
|
||||
@Column(name = "side", nullable = false, length = 8)
|
||||
private String side;
|
||||
|
||||
@Column(name = "order_kind", nullable = false, length = 16)
|
||||
@Builder.Default
|
||||
private String orderKind = "market";
|
||||
|
||||
@Column(name = "source", nullable = false, length = 32)
|
||||
@Builder.Default
|
||||
private String source = "STRATEGY";
|
||||
|
||||
@Column(name = "strategy_id")
|
||||
private Long strategyId;
|
||||
|
||||
@Column(name = "upbit_order_uuid", length = 64)
|
||||
private String upbitOrderUuid;
|
||||
|
||||
@Column(name = "price", nullable = false, precision = 20, scale = 8)
|
||||
private BigDecimal price;
|
||||
|
||||
@Column(name = "quantity", nullable = false, precision = 24, scale = 12)
|
||||
private BigDecimal quantity;
|
||||
|
||||
@Column(name = "gross_amount", nullable = false, precision = 20, scale = 2)
|
||||
private BigDecimal grossAmount;
|
||||
|
||||
@Column(name = "fee_amount", precision = 20, scale = 2)
|
||||
private BigDecimal feeAmount;
|
||||
|
||||
@Column(name = "state", nullable = false, length = 24)
|
||||
@Builder.Default
|
||||
private String state = "done";
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gc_paper_account")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcPaperAccount {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "device_id", length = 100, nullable = false, unique = true)
|
||||
private String deviceId;
|
||||
|
||||
@Column(name = "cash_balance", precision = 20, scale = 2, nullable = false)
|
||||
@Builder.Default
|
||||
private BigDecimal cashBalance = BigDecimal.valueOf(10_000_000);
|
||||
|
||||
@Column(name = "realized_pnl", precision = 20, scale = 2, nullable = false)
|
||||
@Builder.Default
|
||||
private BigDecimal realizedPnl = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gc_paper_position",
|
||||
uniqueConstraints = @UniqueConstraint(name = "uk_paper_position_account_symbol",
|
||||
columnNames = {"account_id", "symbol"}))
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcPaperPosition {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "account_id", nullable = false)
|
||||
private Long accountId;
|
||||
|
||||
@Column(name = "symbol", length = 50, nullable = false)
|
||||
private String symbol;
|
||||
|
||||
@Column(name = "korean_name", length = 100)
|
||||
private String koreanName;
|
||||
|
||||
@Column(name = "quantity", precision = 30, scale = 12, nullable = false)
|
||||
@Builder.Default
|
||||
private BigDecimal quantity = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "avg_price", precision = 20, scale = 2, nullable = false)
|
||||
@Builder.Default
|
||||
private BigDecimal avgPrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gc_paper_trade")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcPaperTrade {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "account_id", nullable = false)
|
||||
private Long accountId;
|
||||
|
||||
@Column(name = "symbol", length = 50, nullable = false)
|
||||
private String symbol;
|
||||
|
||||
@Column(name = "side", length = 10, nullable = false)
|
||||
private String side;
|
||||
|
||||
@Column(name = "order_kind", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String orderKind = "limit";
|
||||
|
||||
@Column(name = "source", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String source = "MANUAL";
|
||||
|
||||
@Column(name = "strategy_id")
|
||||
private Long strategyId;
|
||||
|
||||
@Column(name = "price", precision = 20, scale = 2, nullable = false)
|
||||
private BigDecimal price;
|
||||
|
||||
@Column(name = "quantity", precision = 30, scale = 12, nullable = false)
|
||||
private BigDecimal quantity;
|
||||
|
||||
@Column(name = "gross_amount", precision = 20, scale = 2, nullable = false)
|
||||
private BigDecimal grossAmount;
|
||||
|
||||
@Column(name = "fee_amount", precision = 20, scale = 2, nullable = false)
|
||||
@Builder.Default
|
||||
private BigDecimal feeAmount = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "net_amount", precision = 20, scale = 2, nullable = false)
|
||||
private BigDecimal netAmount;
|
||||
|
||||
@Column(name = "cash_after", precision = 20, scale = 2, nullable = false)
|
||||
private BigDecimal cashAfter;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gc_role_menu_permission",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"role", "menu_id"}))
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcRoleMenuPermission {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String role;
|
||||
|
||||
@Column(name = "menu_id", nullable = false, length = 50)
|
||||
private String menuId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean allowed = true;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* GoldenChart 투자전략 DSL 엔티티.
|
||||
* frontend StrategyPage 에서 작성한 LogicNode 트리를 JSON 으로 저장.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_strategy")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcStrategy {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** 로그인 사용자 ID (nullable — 비회원은 device_id 사용) */
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
/** 비회원 기기 식별자 */
|
||||
@Column(name = "device_id", length = 100)
|
||||
private String deviceId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 200)
|
||||
private String name;
|
||||
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/** 매수 조건 DSL JSON — frontend LogicNode 구조 */
|
||||
@Column(name = "buy_condition_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String buyConditionJson;
|
||||
|
||||
/** 매도 조건 DSL JSON — frontend LogicNode 구조 */
|
||||
@Column(name = "sell_condition_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String sellConditionJson;
|
||||
|
||||
@Column(name = "enabled", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean enabled = true;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 실시간 전략 체크에서 발생한 매매 시그널 이력.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_trade_signal")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcTradeSignal {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "device_id", length = 100)
|
||||
private String deviceId;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "market", nullable = false, length = 30)
|
||||
private String market;
|
||||
|
||||
@Column(name = "strategy_id")
|
||||
private Long strategyId;
|
||||
|
||||
@Column(name = "strategy_name", length = 200)
|
||||
private String strategyName;
|
||||
|
||||
/** BUY | SELL */
|
||||
@Column(name = "signal_type", nullable = false, length = 10)
|
||||
private String signalType;
|
||||
|
||||
@Column(name = "price", nullable = false)
|
||||
private Double price;
|
||||
|
||||
/** 캔들 시작 Unix 초 */
|
||||
@Column(name = "candle_time", nullable = false)
|
||||
private Long candleTime;
|
||||
|
||||
@Column(name = "candle_type", nullable = false, length = 10)
|
||||
@Builder.Default
|
||||
private String candleType = "1m";
|
||||
|
||||
/** CANDLE_CLOSE | REALTIME_TICK */
|
||||
@Column(name = "execution_type", nullable = false, length = 30)
|
||||
private String executionType;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() { createdAt = LocalDateTime.now(); }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gc_user")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcUser {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String username;
|
||||
|
||||
@Column(name = "password_hash", nullable = false, length = 100)
|
||||
private String passwordHash;
|
||||
|
||||
@Column(name = "display_name", length = 100)
|
||||
private String displayName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean enabled = true;
|
||||
|
||||
/** ADMIN | USER | GUEST */
|
||||
@Column(nullable = false, length = 20)
|
||||
private String role = "USER";
|
||||
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
void prePersist() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (createdAt == null) createdAt = now;
|
||||
updatedAt = now;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
void preUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.goldenchart.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 관심종목 (워치리스트).
|
||||
* 기존 crypto_favorites 와 별도로 GoldenChart 전용 관리.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "gc_watchlist",
|
||||
uniqueConstraints = @UniqueConstraint(name = "uk_watchlist_user_symbol",
|
||||
columnNames = {"user_id", "device_id", "symbol"}))
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class GcWatchlist {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "device_id", length = 100)
|
||||
private String deviceId;
|
||||
|
||||
@Column(name = "symbol", length = 50, nullable = false)
|
||||
private String symbol;
|
||||
|
||||
@Column(name = "korean_name", length = 100)
|
||||
private String koreanName;
|
||||
|
||||
@Column(name = "english_name", length = 100)
|
||||
private String englishName;
|
||||
|
||||
@Column(name = "display_order")
|
||||
@Builder.Default
|
||||
private Integer displayOrder = 0;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcAppSettings;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface GcAppSettingsRepository extends JpaRepository<GcAppSettings, Long> {
|
||||
|
||||
Optional<GcAppSettings> findByDeviceId(String deviceId);
|
||||
|
||||
Optional<GcAppSettings> findByUserId(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcBacktestResult;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface GcBacktestResultRepository extends JpaRepository<GcBacktestResult, Long> {
|
||||
List<GcBacktestResult> findByDeviceIdOrderByCreatedAtDesc(String deviceId);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcBacktestSettings;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GcBacktestSettingsRepository extends JpaRepository<GcBacktestSettings, Long> {
|
||||
Optional<GcBacktestSettings> findFirstByDeviceIdOrderByUpdatedAtDesc(String deviceId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcChartWorkspace;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GcChartWorkspaceRepository extends JpaRepository<GcChartWorkspace, Long> {
|
||||
|
||||
Optional<GcChartWorkspace> findByUserId(Long userId);
|
||||
|
||||
Optional<GcChartWorkspace> findByDeviceId(String deviceId);
|
||||
|
||||
@Query("SELECT w FROM GcChartWorkspace w LEFT JOIN FETCH w.slots WHERE w.userId = :userId")
|
||||
Optional<GcChartWorkspace> findWithSlotsByUserId(Long userId);
|
||||
|
||||
@Query("SELECT w FROM GcChartWorkspace w LEFT JOIN FETCH w.slots WHERE w.deviceId = :deviceId")
|
||||
Optional<GcChartWorkspace> findWithSlotsByDeviceId(String deviceId);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcFcmToken;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GcFcmTokenRepository extends JpaRepository<GcFcmToken, Long> {
|
||||
|
||||
Optional<GcFcmToken> findByToken(String token);
|
||||
|
||||
List<GcFcmToken> findByActiveTrue();
|
||||
|
||||
List<GcFcmToken> findByDeviceIdAndActiveTrue(String deviceId);
|
||||
|
||||
void deleteByDeviceId(String deviceId);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcHoldings;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface GcHoldingsRepository extends JpaRepository<GcHoldings, Long> {
|
||||
List<GcHoldings> findByUserIdOrderByDisplayOrderAsc(Long userId);
|
||||
List<GcHoldings> findByDeviceIdOrderByDisplayOrderAsc(String deviceId);
|
||||
void deleteByUserIdAndSymbol(Long userId, String symbol);
|
||||
void deleteByDeviceIdAndSymbol(String deviceId, String symbol);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcIndicatorSettings;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface GcIndicatorSettingsRepository extends JpaRepository<GcIndicatorSettings, Long> {
|
||||
|
||||
Optional<GcIndicatorSettings> findByDeviceId(String deviceId);
|
||||
|
||||
Optional<GcIndicatorSettings> findByUserId(Long userId);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcLiveStrategySettings;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GcLiveStrategySettingsRepository
|
||||
extends JpaRepository<GcLiveStrategySettings, Long> {
|
||||
|
||||
Optional<GcLiveStrategySettings> findByUserIdAndMarket(Long userId, String market);
|
||||
|
||||
Optional<GcLiveStrategySettings> findByDeviceIdAndMarket(String deviceId, String market);
|
||||
|
||||
/** isLiveCheck = true 인 모든 설정 (스케줄러 순회용) */
|
||||
List<GcLiveStrategySettings> findAllByIsLiveCheckTrue();
|
||||
|
||||
/** 디바이스별 실시간 체크 ON 설정 */
|
||||
List<GcLiveStrategySettings> findByDeviceIdAndIsLiveCheckTrue(String deviceId);
|
||||
|
||||
/** 유저별 실시간 체크 ON 설정 */
|
||||
List<GcLiveStrategySettings> findByUserIdAndIsLiveCheckTrue(Long userId);
|
||||
|
||||
/** 특정 마켓의 활성 설정 전체 (여러 디바이스가 동일 마켓 구독 시) */
|
||||
@Query("SELECT s FROM GcLiveStrategySettings s WHERE s.market = :market AND s.isLiveCheck = true")
|
||||
List<GcLiveStrategySettings> findActiveByMarket(@Param("market") String market);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcLiveTrade;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GcLiveTradeRepository extends JpaRepository<GcLiveTrade, Long> {
|
||||
List<GcLiveTrade> findTop100ByDeviceIdOrderByCreatedAtDesc(String deviceId);
|
||||
Optional<GcLiveTrade> findTopByDeviceIdAndSymbolAndSideOrderByCreatedAtDesc(
|
||||
String deviceId, String symbol, String side);
|
||||
List<GcLiveTrade> findBySymbolAndSideOrderByCreatedAtDesc(String symbol, String side);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcPaperAccount;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GcPaperAccountRepository extends JpaRepository<GcPaperAccount, Long> {
|
||||
Optional<GcPaperAccount> findByDeviceId(String deviceId);
|
||||
Optional<GcPaperAccount> findByUserId(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcPaperPosition;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GcPaperPositionRepository extends JpaRepository<GcPaperPosition, Long> {
|
||||
List<GcPaperPosition> findByAccountIdOrderBySymbolAsc(Long accountId);
|
||||
Optional<GcPaperPosition> findByAccountIdAndSymbol(Long accountId, String symbol);
|
||||
/** 실시간 손절/익절 — 종목별 보유 포지션 전체 */
|
||||
List<GcPaperPosition> findBySymbolAndQuantityGreaterThan(String symbol, java.math.BigDecimal minQty);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcPaperTrade;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface GcPaperTradeRepository extends JpaRepository<GcPaperTrade, Long> {
|
||||
List<GcPaperTrade> findTop100ByAccountIdOrderByCreatedAtDesc(Long accountId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcRoleMenuPermission;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface GcRoleMenuPermissionRepository extends JpaRepository<GcRoleMenuPermission, Long> {
|
||||
List<GcRoleMenuPermission> findByRole(String role);
|
||||
void deleteByRole(String role);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcStrategy;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface GcStrategyRepository extends JpaRepository<GcStrategy, Long> {
|
||||
|
||||
/** 사용자 ID 기준 전략 목록 조회 */
|
||||
List<GcStrategy> findByUserIdOrderByUpdatedAtDesc(Long userId);
|
||||
|
||||
/** 기기 ID 기준 전략 목록 조회 (비회원) */
|
||||
List<GcStrategy> findByDeviceIdOrderByUpdatedAtDesc(String deviceId);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcTradeSignal;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface GcTradeSignalRepository extends JpaRepository<GcTradeSignal, Long> {
|
||||
|
||||
List<GcTradeSignal> findByDeviceIdOrderByCreatedAtDesc(String deviceId);
|
||||
List<GcTradeSignal> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
List<GcTradeSignal> findByDeviceIdAndMarketOrderByCreatedAtDesc(String deviceId, String market);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcUser;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GcUserRepository extends JpaRepository<GcUser, Long> {
|
||||
Optional<GcUser> findByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.goldenchart.repository;
|
||||
|
||||
import com.goldenchart.entity.GcWatchlist;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface GcWatchlistRepository extends JpaRepository<GcWatchlist, Long> {
|
||||
List<GcWatchlist> findByUserIdOrderByDisplayOrderAsc(Long userId);
|
||||
List<GcWatchlist> findByDeviceIdOrderByDisplayOrderAsc(String deviceId);
|
||||
void deleteByUserIdAndSymbol(Long userId, String symbol);
|
||||
void deleteByDeviceIdAndSymbol(String deviceId, String symbol);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user