goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
frontend_golden/
node_modules/
.git/
deploy-home.sh
# ta4j-master 는 backend 빌드에 필요하므로 포함
+17
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
target/
*.log
.mvn/
+42
View File
@@ -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
View File
@@ -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&lt;String, Map&lt;String, Object&gt;&gt; 형태로 역직렬화한다.
*/
@Column(name = "params_json", columnDefinition = "JSON", nullable = false)
@JdbcTypeCode(SqlTypes.JSON)
@Builder.Default
private String paramsJson = "{}";
/**
* 지표 타입 → 시각 설정(색상·선굵기·수평선) JSON 직렬화.
* Map&lt;String, IndicatorVisual&gt; 형태로 역직렬화한다.
* <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);
}
@@ -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