알림 시간봉 오류 수정

This commit is contained in:
Macbook
2026-05-29 20:42:35 +09:00
parent a5f0a7eefd
commit 775f83e1b3
13 changed files with 200 additions and 35 deletions
@@ -5,6 +5,7 @@ import com.goldenchart.entity.GcWatchlist;
import com.goldenchart.repository.GcWatchlistRepository;
import com.goldenchart.service.AppSettingsService;
import com.goldenchart.service.LiveStrategyEvaluator;
import com.goldenchart.service.LiveStrategySettingsService;
import com.goldenchart.service.StrategyConditionTimeframeService;
import com.goldenchart.websocket.DynamicSubscriptionManager;
import com.goldenchart.websocket.UpbitWebSocketClient;
@@ -27,11 +28,13 @@ public class LiveStrategyStartupRunner implements ApplicationRunner {
private final GcWatchlistRepository watchlistRepo;
private final DynamicSubscriptionManager subscriptionManager;
private final LiveStrategyEvaluator liveStrategyEvaluator;
private final LiveStrategySettingsService liveStrategySettingsService;
private final StrategyConditionTimeframeService conditionTimeframes;
private final UpbitWebSocketClient upbitWebSocketClient;
@Override
public void run(ApplicationArguments args) {
liveStrategySettingsService.reconcileOrphanAndStaleSettings();
// 디바이스별 전역 설정이 켜져 있고 전략이 지정된 경우, DB 관심종목 전체 고정 구독
int pinned = 0;
for (GcWatchlist w : watchlistRepo.findAll()) {
@@ -44,4 +44,11 @@ public class CandleBarDto {
/** 시그널이 발생한 평가 분봉 (예: 1m, 3m, 5m) */
private String candleType;
/** 시그널 소유 — STOMP 수신 클라이언트가 본인 알림만 표시할 때 사용 */
private Long strategyId;
private Long userId;
private String deviceId;
/** CANDLE_CLOSE | REALTIME_TICK */
private String executionType;
}
@@ -13,6 +13,7 @@ public interface GcTradeSignalRepository extends JpaRepository<GcTradeSignal, Lo
List<GcTradeSignal> findByDeviceIdOrderByCreatedAtDesc(String deviceId);
List<GcTradeSignal> findByUserIdOrderByCreatedAtDesc(Long userId);
List<GcTradeSignal> findByDeviceIdAndMarketOrderByCreatedAtDesc(String deviceId, String market);
List<GcTradeSignal> findByUserIdAndMarketOrderByCreatedAtDesc(Long userId, String market);
@Modifying
@Query("DELETE FROM GcTradeSignal e WHERE e.userId = :userId")
@@ -6,6 +6,7 @@ import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.entity.GcWatchlist;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
import com.goldenchart.repository.GcStrategyRepository;
import com.goldenchart.repository.GcWatchlistRepository;
import com.goldenchart.websocket.DynamicSubscriptionManager;
import lombok.RequiredArgsConstructor;
@@ -26,6 +27,7 @@ import java.util.Optional;
public class LiveStrategySettingsService {
private final GcLiveStrategySettingsRepository repo;
private final GcStrategyRepository strategyRepo;
private final GcWatchlistRepository watchlistRepo;
private final AppSettingsService appSettingsService;
private final DynamicSubscriptionManager subscriptionManager;
@@ -35,10 +37,11 @@ public class LiveStrategySettingsService {
// ── 조회 ──────────────────────────────────────────────────────────────────
@Transactional(readOnly = true)
@Transactional
public LiveStrategySettingsDto get(Long userId, String deviceId, String market) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
Optional<GcLiveStrategySettings> entity = findEntity(userId, deviceId, market);
entity.ifPresent(this::reconcileOneRow);
LiveStrategySettingsDto dto = entity.isPresent()
? toDto(entity.get())
: defaultDtoFromApp(market, app);
@@ -48,12 +51,15 @@ public class LiveStrategySettingsService {
return dto;
}
@Transactional(readOnly = true)
@Transactional
public List<LiveStrategySettingsDto> listActive(Long userId, String deviceId) {
List<GcLiveStrategySettings> rows = userId != null
? repo.findByUserIdAndIsLiveCheckTrue(userId)
: repo.findByDeviceIdAndIsLiveCheckTrue(
deviceId != null && !deviceId.isBlank() ? deviceId : "anonymous");
for (GcLiveStrategySettings row : rows) {
reconcileOneRow(row);
}
if (rows.isEmpty()) {
return List.of();
}
@@ -116,6 +122,61 @@ public class LiveStrategySettingsService {
// ── 저장/갱신 — 전역 템플릿 + 관심종목 전체 동기화 ─────────────────────────
/**
* 기동·설정 조회 시 — strategy_id 누락·삭제 전략·회원 전역 전략 불일치 보정.
*
* @return 수정된 행 수
*/
@Transactional
public int reconcileOrphanAndStaleSettings() {
int fixed = 0;
for (GcLiveStrategySettings row : repo.findAllByIsLiveCheckTrue()) {
if (reconcileOneRow(row)) {
fixed++;
}
}
if (fixed > 0) {
log.info("[LiveStrategySettings] reconcile fixed {} active row(s)", fixed);
}
return fixed;
}
private boolean reconcileOneRow(GcLiveStrategySettings row) {
boolean changed = false;
GcAppSettings app = appSettingsService.getEntity(row.getUserId(), row.getDeviceId());
if (row.getStrategyId() == null) {
row.setIsLiveCheck(false);
changed = true;
} else if (!strategyRepo.existsById(row.getStrategyId())) {
if (row.getUserId() != null
&& Boolean.TRUE.equals(app.getLiveStrategyCheck())
&& app.getLiveStrategyId() != null
&& strategyRepo.existsById(app.getLiveStrategyId())) {
row.setStrategyId(app.getLiveStrategyId());
} else {
row.setIsLiveCheck(false);
}
changed = true;
} else if (row.getUserId() != null
&& Boolean.TRUE.equals(app.getLiveStrategyCheck())
&& app.getLiveStrategyId() != null
&& !app.getLiveStrategyId().equals(row.getStrategyId())
&& strategyRepo.existsById(app.getLiveStrategyId())) {
row.setStrategyId(app.getLiveStrategyId());
conditionTimeframes.invalidate(app.getLiveStrategyId());
changed = true;
}
if (changed) {
repo.save(row);
if (row.getMarket() != null) {
liveStrategyEvaluator.invalidateCache(row.getMarket());
}
}
return changed;
}
public LiveStrategySettingsDto save(Long userId, String deviceId,
LiveStrategySettingsDto dto) {
if (!Boolean.TRUE.equals(dto.getSkipGlobalTemplate())) {
@@ -2,6 +2,7 @@ package com.goldenchart.service;
import com.goldenchart.dto.TradeSignalDto;
import com.goldenchart.entity.GcTradeSignal;
import com.goldenchart.repository.GcStrategyRepository;
import com.goldenchart.repository.GcTradeSignalRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -22,6 +23,7 @@ import java.util.List;
public class TradeSignalService {
private final GcTradeSignalRepository repo;
private final GcStrategyRepository strategyRepo;
private final FcmPushService fcmPushService;
// ── 저장 ─────────────────────────────────────────────────────────────────
@@ -31,12 +33,13 @@ public class TradeSignalService {
String signalType, double price,
long candleTime, String candleType,
String executionType) {
String resolvedName = resolveStrategyName(strategyId, strategyName);
GcTradeSignal entity = GcTradeSignal.builder()
.deviceId(deviceId)
.userId(userId)
.market(market)
.strategyId(strategyId)
.strategyName(strategyName)
.strategyName(resolvedName)
.signalType(signalType)
.price(price)
.candleTime(candleTime)
@@ -49,7 +52,7 @@ public class TradeSignalService {
// 로그인 사용자는 live 설정에 userId만 있고 deviceId가 null — FCM은 userId 기준으로 발송
if (userId != null || (deviceId != null && !deviceId.isBlank())) {
fcmPushService.sendTradeSignalIfEnabled(
deviceId, userId, market, signalType, price, strategyName, saved.getId());
deviceId, userId, market, signalType, price, resolvedName, saved.getId());
}
return toDto(saved);
}
@@ -66,9 +69,11 @@ public class TradeSignalService {
@Transactional(readOnly = true)
public List<TradeSignalDto> listByMarket(Long userId, String deviceId, String market) {
return repo.findByDeviceIdAndMarketOrderByCreatedAtDesc(
deviceId != null ? deviceId : "", market)
.stream().map(this::toDto).toList();
List<GcTradeSignal> list = userId != null
? repo.findByUserIdAndMarketOrderByCreatedAtDesc(userId, market)
: repo.findByDeviceIdAndMarketOrderByCreatedAtDesc(
deviceId != null ? deviceId : "", market);
return list.stream().map(this::toDto).toList();
}
// ── 삭제 ─────────────────────────────────────────────────────────────────
@@ -117,6 +122,14 @@ public class TradeSignalService {
return deviceId.equals(entity.getDeviceId());
}
private String resolveStrategyName(Long strategyId, String strategyName) {
if (strategyName != null && !strategyName.isBlank()) return strategyName;
if (strategyId == null) return null;
return strategyRepo.findById(strategyId)
.map(s -> s.getName())
.orElse(null);
}
// ── 변환 ─────────────────────────────────────────────────────────────────
private TradeSignalDto toDto(GcTradeSignal e) {
@@ -196,7 +196,7 @@ public class BarBuilder {
if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) continue;
publishStrategySignal(market, candleType, signalBar, closeSignal);
publishStrategySignal(market, candleType, signalBar, closeSignal, s, signalExecType);
try {
tradeSignalService.save(
s.getDeviceId(), s.getUserId(),
@@ -216,8 +216,9 @@ public class BarBuilder {
}
}
/** 평가 분봉 STOMP 채널로 봉 마감 시그널 발행 */
private void publishStrategySignal(String market, String candleType, Bar bar, String signal) {
/** 평가 분봉 STOMP 채널로 봉 마감 시그널 발행 (소유자·전략 메타 포함) */
private void publishStrategySignal(String market, String candleType, Bar bar, String signal,
GcLiveStrategySettings setting, String executionType) {
long barStartEpoch = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
CandleBarDto dto = CandleBarDto.builder()
.time(barStartEpoch)
@@ -228,9 +229,14 @@ public class BarBuilder {
.volume(bar.getVolume().doubleValue())
.signal(signal)
.candleType(candleType)
.strategyId(setting.getStrategyId())
.userId(setting.getUserId())
.deviceId(setting.getDeviceId())
.executionType(executionType)
.build();
broker.publish(market, candleType, dto);
log.info("[BarBuilder] bar-close signal={} market={} candleType={}", signal, market, candleType);
log.info("[BarBuilder] bar-close signal={} market={} candleType={} strategyId={} userId={}",
signal, market, candleType, setting.getStrategyId(), setting.getUserId());
}
/** 현재 진행 중인 1분봉을 STOMP 로 발행 (실시간 틱 렌더링용, 시그널 없음). */
@@ -86,6 +86,10 @@ public class LiveStrategyScheduler {
.volume(lastBar.getVolume().doubleValue())
.signal(signal)
.candleType(candleType)
.strategyId(s.getStrategyId())
.userId(s.getUserId())
.deviceId(s.getDeviceId())
.executionType("REALTIME_TICK")
.build();
broker.publish(market, candleType, dto);