Files
goldenChart/backend/src/main/java/com/goldenchart/service/LiveStrategySettingsService.java
T
2026-05-25 03:15:05 +09:00

299 lines
14 KiB
Java

package com.goldenchart.service;
import com.goldenchart.dto.LiveStrategyBulkRequest;
import com.goldenchart.dto.LiveStrategySettingsDto;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.entity.GcWatchlist;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
import com.goldenchart.repository.GcWatchlistRepository;
import com.goldenchart.websocket.DynamicSubscriptionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class LiveStrategySettingsService {
private final GcLiveStrategySettingsRepository repo;
private final GcWatchlistRepository watchlistRepo;
private final AppSettingsService appSettingsService;
private final DynamicSubscriptionManager subscriptionManager;
private final LiveStrategyEvaluator liveStrategyEvaluator;
private final StrategyConditionTimeframeService conditionTimeframes;
// ── 조회 ──────────────────────────────────────────────────────────────────
@Transactional(readOnly = true)
public LiveStrategySettingsDto get(Long userId, String deviceId, String market) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
Optional<GcLiveStrategySettings> entity = findEntity(userId, deviceId, market);
LiveStrategySettingsDto dto = entity.isPresent()
? toDto(entity.get())
: defaultDtoFromApp(market, app);
// 실시간 체크 마스터 스위치·전략 ID는 앱 전역 설정(gc_app_settings) 기준
dto = mergeGlobalTemplate(dto, app, market);
enrichStrategyTimeframes(dto);
return dto;
}
@Transactional(readOnly = true)
public List<LiveStrategySettingsDto> listActive(Long userId, String deviceId) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) || app.getLiveStrategyId() == null) {
return List.of();
}
List<String> watchlist = listWatchlistSymbols(userId, deviceId);
Long strategyId = app.getLiveStrategyId();
List<String> strategyCandleTypes = conditionTimeframes.collectForStrategyList(strategyId);
return watchlist.stream()
.map(m -> LiveStrategySettingsDto.builder()
.market(m)
.strategyId(strategyId)
.liveCheck(true)
.executionType(app.getLiveExecutionType())
.positionMode(app.getLivePositionMode())
.strategyCandleTypes(strategyCandleTypes)
.build())
.toList();
}
// ── 관심종목 연동 (WatchlistService 에서 호출) ─────────────────────────────
/** 관심종목 등록 시 — DB 관심종목 = 전략 체크 대상 행 생성/갱신 */
public void enableForWatchlistMarket(Long userId, String deviceId, String market) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
LiveStrategySettingsDto template = findEntity(userId, deviceId, market)
.map(e -> mergeGlobalTemplate(toDto(e), app, market))
.orElseGet(() -> mergeGlobalTemplate(defaultDtoFromApp(market, app), app, market));
template.setLiveCheck(true);
enrichStrategyTimeframes(template);
upsertEntity(userId, deviceId, template);
pinIfReady(market, template);
log.debug("[LiveStrategy] watchlist add → enabled {}", market);
}
/** 관심종목 해제 시 — 해당 종목 전략 체크 비활성화 */
public void disableForWatchlistMarket(Long userId, String deviceId, String market) {
findEntity(userId, deviceId, market).ifPresent(entity -> {
entity.setIsLiveCheck(false);
repo.save(entity);
liveStrategyEvaluator.invalidateCache(market);
});
log.debug("[LiveStrategy] watchlist remove → disabled {}", market);
}
// ── 일괄 저장 (레거시 API) ─────────────────────────────────────────────────
public List<LiveStrategySettingsDto> saveBulk(Long userId, String deviceId,
LiveStrategyBulkRequest req) {
if (req.getMarkets() == null || req.getMarkets().isEmpty()) {
return List.of();
}
LiveStrategySettingsDto template = req.getTemplate() != null
? req.getTemplate()
: defaultDtoFromApp("KRW-BTC", appSettingsService.getEntity(userId, deviceId));
persistGlobalTemplate(userId, deviceId, template);
List<LiveStrategySettingsDto> results = new ArrayList<>();
for (String market : req.getMarkets()) {
if (market == null || market.isBlank()) continue;
LiveStrategySettingsDto one = LiveStrategySettingsDto.builder()
.market(market)
.strategyId(template.getStrategyId())
.liveCheck(true)
.executionType(template.getExecutionType())
.positionMode(template.getPositionMode())
.build();
results.add(upsertEntity(userId, deviceId, one));
}
log.info("[LiveStrategySettings] bulk save: {} markets", results.size());
return results;
}
// ── 저장/갱신 — 전역 템플릿 + 관심종목 전체 동기화 ─────────────────────────
public LiveStrategySettingsDto save(Long userId, String deviceId,
LiveStrategySettingsDto dto) {
persistGlobalTemplate(userId, deviceId, dto);
syncAllWatchlistMarkets(userId, deviceId, dto);
String market = dto.getMarket() != null ? dto.getMarket() : "KRW-BTC";
log.info("[LiveStrategySettings] global saved + watchlist sync, template market={}", market);
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
LiveStrategySettingsDto out = findEntity(userId, deviceId, market)
.map(this::toDto)
.orElseGet(() -> defaultDtoFromApp(market, app));
out = mergeGlobalTemplate(out, app, market);
if (dto.getExecutionType() != null) {
out.setExecutionType(dto.getExecutionType());
}
if (dto.getPositionMode() != null) {
out.setPositionMode(dto.getPositionMode());
}
enrichStrategyTimeframes(out);
return out;
}
// ── Private ───────────────────────────────────────────────────────────────
private void persistGlobalTemplate(Long userId, String deviceId, LiveStrategySettingsDto dto) {
Map<String, Object> patch = new HashMap<>();
patch.put("liveStrategyCheck", dto.isLiveCheck());
patch.put("liveStrategyId", dto.getStrategyId());
patch.put("liveExecutionType", dto.getExecutionType());
patch.put("livePositionMode", dto.getPositionMode());
appSettingsService.save(userId, deviceId, patch);
}
private void syncAllWatchlistMarkets(Long userId, String deviceId,
LiveStrategySettingsDto template) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
boolean active = Boolean.TRUE.equals(app.getLiveStrategyCheck())
&& app.getLiveStrategyId() != null;
for (String symbol : listWatchlistSymbols(userId, deviceId)) {
LiveStrategySettingsDto row = findEntity(userId, deviceId, symbol)
.map(e -> mergeGlobalTemplate(toDto(e), app, symbol))
.orElseGet(() -> defaultDtoFromApp(symbol, app));
row.setLiveCheck(active);
row.setStrategyId(app.getLiveStrategyId());
row.setExecutionType(app.getLiveExecutionType() != null
? app.getLiveExecutionType() : row.getExecutionType());
row.setPositionMode(app.getLivePositionMode() != null
? app.getLivePositionMode() : row.getPositionMode());
enrichStrategyTimeframes(row);
LiveStrategySettingsDto saved = upsertEntity(userId, deviceId, row);
if (active) {
pinIfReady(symbol, saved);
}
}
}
private void enrichStrategyTimeframes(LiveStrategySettingsDto dto) {
if (dto.getStrategyId() != null) {
dto.setStrategyCandleTypes(
conditionTimeframes.collectForStrategyList(dto.getStrategyId()));
}
}
private void pinAllStrategyTimeframes(String market, Long strategyId) {
if (strategyId == null) return;
subscriptionManager.ensureMarketPinned(market, "1m");
for (String ct : conditionTimeframes.collectForStrategy(strategyId)) {
if (!"1m".equals(ct)) {
subscriptionManager.ensureMarketPinned(market, ct);
}
}
}
private LiveStrategySettingsDto upsertEntity(Long userId, String deviceId,
LiveStrategySettingsDto dto) {
String market = dto.getMarket() != null ? dto.getMarket() : "KRW-BTC";
GcLiveStrategySettings entity = findEntity(userId, deviceId, market)
.orElseGet(() -> GcLiveStrategySettings.builder()
.userId(userId)
.deviceId(userId == null ? deviceId : null)
.market(market)
.build());
applyOwnerKeys(entity, userId, deviceId);
entity.setStrategyId(dto.getStrategyId());
entity.setIsLiveCheck(dto.isLiveCheck());
entity.setExecutionType(
"REALTIME_TICK".equals(dto.getExecutionType()) ? "REALTIME_TICK" : "CANDLE_CLOSE");
entity.setPositionMode(
"SIGNAL_ONLY".equals(dto.getPositionMode()) ? "SIGNAL_ONLY" : "LONG_ONLY");
repo.save(entity);
liveStrategyEvaluator.invalidateCache(market);
return toDto(entity);
}
private void pinIfReady(String market, LiveStrategySettingsDto dto) {
if (Boolean.TRUE.equals(dto.isLiveCheck()) && dto.getStrategyId() != null) {
pinAllStrategyTimeframes(market, dto.getStrategyId());
}
}
private List<String> listWatchlistSymbols(Long userId, String deviceId) {
List<GcWatchlist> list = userId != null
? watchlistRepo.findByUserIdOrderByDisplayOrderAsc(userId)
: watchlistRepo.findByDeviceIdOrderByDisplayOrderAsc(
deviceId != null ? deviceId : "anonymous");
return list.stream().map(GcWatchlist::getSymbol).toList();
}
/**
* user_id·device_id 어느 쪽으로 저장됐든 동일 마켓 행을 찾는다.
* (게스트 → 로그인 전환 시 device_id 행만 있어도 upsert 가 INSERT 로 중복 나지 않게)
*/
private Optional<GcLiveStrategySettings> findEntity(Long userId, String deviceId,
String market) {
if (userId != null) {
Optional<GcLiveStrategySettings> byUser = repo.findByUserIdAndMarket(userId, market);
if (byUser.isPresent()) return byUser;
}
if (deviceId != null && !deviceId.isBlank()) {
return repo.findByDeviceIdAndMarket(deviceId, market);
}
return Optional.empty();
}
/** 회원: user_id 전용 행 / 비회원: device_id 전용 행 (UK 충돌 방지) */
private void applyOwnerKeys(GcLiveStrategySettings entity, Long userId, String deviceId) {
if (userId != null) {
entity.setUserId(userId);
entity.setDeviceId(null);
} else {
entity.setUserId(null);
entity.setDeviceId(deviceId != null && !deviceId.isBlank() ? deviceId : null);
}
}
private LiveStrategySettingsDto toDto(GcLiveStrategySettings e) {
LiveStrategySettingsDto dto = LiveStrategySettingsDto.builder()
.market(e.getMarket())
.strategyId(e.getStrategyId())
.liveCheck(Boolean.TRUE.equals(e.getIsLiveCheck()))
.executionType(e.getExecutionType())
.positionMode(e.getPositionMode() != null ? e.getPositionMode() : "LONG_ONLY")
.build();
enrichStrategyTimeframes(dto);
return dto;
}
/** 앱 전역 실시간 체크 ON/OFF·전략 ID를 DTO에 반영 (종목별 행과 분리) */
private LiveStrategySettingsDto mergeGlobalTemplate(
LiveStrategySettingsDto dto, GcAppSettings app, String market) {
dto.setMarket(market);
dto.setLiveCheck(Boolean.TRUE.equals(app.getLiveStrategyCheck()));
dto.setStrategyId(app.getLiveStrategyId());
if (app.getLiveExecutionType() != null) {
dto.setExecutionType(app.getLiveExecutionType());
}
if (app.getLivePositionMode() != null) {
dto.setPositionMode(app.getLivePositionMode());
}
return dto;
}
private LiveStrategySettingsDto defaultDtoFromApp(String market, GcAppSettings app) {
return LiveStrategySettingsDto.builder()
.market(market)
.strategyId(app.getLiveStrategyId())
.liveCheck(Boolean.TRUE.equals(app.getLiveStrategyCheck()))
.executionType(app.getLiveExecutionType() != null ? app.getLiveExecutionType() : "CANDLE_CLOSE")
.positionMode(app.getLivePositionMode() != null ? app.getLivePositionMode() : "LONG_ONLY")
.build();
}
}