goldenChat base source add
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
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 LiveStrategyTimeframeService timeframeService;
|
||||
|
||||
// ── 조회 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@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) 기준
|
||||
return mergeGlobalTemplate(dto, app, market);
|
||||
}
|
||||
|
||||
@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);
|
||||
return watchlist.stream()
|
||||
.map(m -> {
|
||||
Optional<GcLiveStrategySettings> row = findEntity(userId, deviceId, m);
|
||||
String ct = row.map(GcLiveStrategySettings::getCandleType)
|
||||
.map(LiveStrategyTimeframeService::normalize)
|
||||
.orElseGet(() -> timeframeService.resolveCandleType(m, userId, deviceId));
|
||||
return LiveStrategySettingsDto.builder()
|
||||
.market(m)
|
||||
.strategyId(app.getLiveStrategyId())
|
||||
.liveCheck(true)
|
||||
.executionType(app.getLiveExecutionType())
|
||||
.positionMode(app.getLivePositionMode())
|
||||
.candleType(ct)
|
||||
.build();
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ── 관심종목 연동 (WatchlistService 에서 호출) ─────────────────────────────
|
||||
|
||||
/** 관심종목 등록 시 — DB 관심종목 = 전략 체크 대상 행 생성/갱신 */
|
||||
public void enableForWatchlistMarket(Long userId, String deviceId, String market) {
|
||||
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
|
||||
LiveStrategySettingsDto template = defaultDtoFromApp(market, app);
|
||||
template.setLiveCheck(true);
|
||||
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())
|
||||
.candleType(template.getCandleType() != null ? template.getCandleType() : "1m")
|
||||
.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);
|
||||
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 = mergeGlobalTemplate(defaultDtoFromApp(market, app), app, market);
|
||||
if (dto.getExecutionType() != null) {
|
||||
out.setExecutionType(dto.getExecutionType());
|
||||
}
|
||||
if (dto.getPositionMode() != null) {
|
||||
out.setPositionMode(dto.getPositionMode());
|
||||
}
|
||||
if (dto.getCandleType() != null) {
|
||||
out.setCandleType(LiveStrategyTimeframeService.normalize(dto.getCandleType()));
|
||||
}
|
||||
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) {
|
||||
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
|
||||
boolean active = Boolean.TRUE.equals(app.getLiveStrategyCheck())
|
||||
&& app.getLiveStrategyId() != null;
|
||||
for (String symbol : listWatchlistSymbols(userId, deviceId)) {
|
||||
LiveStrategySettingsDto row = defaultDtoFromApp(symbol, app);
|
||||
row.setLiveCheck(active);
|
||||
LiveStrategySettingsDto saved = upsertEntity(userId, deviceId, row);
|
||||
if (active) {
|
||||
pinIfReady(symbol, saved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
entity.setCandleType(LiveStrategyTimeframeService.normalize(
|
||||
dto.getCandleType() != null ? dto.getCandleType() : "1m"));
|
||||
|
||||
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) {
|
||||
String ct = LiveStrategyTimeframeService.normalize(
|
||||
dto.getCandleType() != null ? dto.getCandleType() : "1m");
|
||||
subscriptionManager.ensureMarketPinned(market, ct);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return 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")
|
||||
.candleType(e.getCandleType() != null ? e.getCandleType() : "1m")
|
||||
.build();
|
||||
}
|
||||
|
||||
/** 앱 전역 실시간 체크 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")
|
||||
.candleType(LiveStrategyTimeframeService.normalize(
|
||||
app.getDefaultTimeframe() != null
|
||||
? mapAppTf(app.getDefaultTimeframe()) : "1m"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String mapAppTf(String tf) {
|
||||
return switch (tf) {
|
||||
case "1m", "3m", "5m", "15m", "30m", "1h", "4h" -> tf;
|
||||
case "1D" -> "1d";
|
||||
default -> "1m";
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user