전략조건 상세설명 기능 추가

This commit is contained in:
Macbook
2026-05-25 03:15:05 +09:00
parent 30dedc4abc
commit 67324ded9d
22 changed files with 1151 additions and 187 deletions
@@ -1,13 +1,11 @@
package com.goldenchart.config;
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.service.AppSettingsService;
import com.goldenchart.service.LiveStrategyEvaluator;
import com.goldenchart.service.LiveStrategyTimeframeService;
import com.goldenchart.service.StrategyConditionTimeframeService;
import com.goldenchart.websocket.DynamicSubscriptionManager;
import com.goldenchart.websocket.UpbitWebSocketClient;
import lombok.RequiredArgsConstructor;
@@ -27,9 +25,9 @@ public class LiveStrategyStartupRunner implements ApplicationRunner {
private final AppSettingsService appSettingsService;
private final GcWatchlistRepository watchlistRepo;
private final GcLiveStrategySettingsRepository liveSettingsRepo;
private final DynamicSubscriptionManager subscriptionManager;
private final LiveStrategyEvaluator liveStrategyEvaluator;
private final StrategyConditionTimeframeService conditionTimeframes;
private final UpbitWebSocketClient upbitWebSocketClient;
@Override
@@ -44,11 +42,7 @@ public class LiveStrategyStartupRunner implements ApplicationRunner {
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) || app.getLiveStrategyId() == null) {
continue;
}
String ct = resolvePinnedCandleType(w);
subscriptionManager.ensureMarketPinned(w.getSymbol(), "1m");
if (!"1m".equals(ct)) {
subscriptionManager.ensureMarketPinned(w.getSymbol(), ct);
}
pinStrategyTimeframes(w.getSymbol(), app.getLiveStrategyId());
liveStrategyEvaluator.invalidateCache(w.getSymbol());
pinned++;
}
@@ -59,19 +53,12 @@ public class LiveStrategyStartupRunner implements ApplicationRunner {
upbitWebSocketClient.resubscribeAll();
}
private String resolvePinnedCandleType(GcWatchlist w) {
if (w.getUserId() != null) {
return liveSettingsRepo.findByUserIdAndMarket(w.getUserId(), w.getSymbol())
.map(GcLiveStrategySettings::getCandleType)
.map(LiveStrategyTimeframeService::normalize)
.orElse("1m");
private void pinStrategyTimeframes(String market, Long strategyId) {
subscriptionManager.ensureMarketPinned(market, "1m");
for (String ct : conditionTimeframes.collectForStrategy(strategyId)) {
if (!"1m".equals(ct)) {
subscriptionManager.ensureMarketPinned(market, ct);
}
}
if (w.getDeviceId() != null) {
return liveSettingsRepo.findByDeviceIdAndMarket(w.getDeviceId(), w.getSymbol())
.map(GcLiveStrategySettings::getCandleType)
.map(LiveStrategyTimeframeService::normalize)
.orElse("1m");
}
return "1m";
}
}
@@ -1,6 +1,7 @@
package com.goldenchart.controller;
import com.goldenchart.dto.StrategyDto;
import com.goldenchart.service.StrategyConditionTimeframeService;
import com.goldenchart.service.StrategyService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
@@ -23,6 +24,7 @@ import java.util.List;
public class StrategyController {
private final StrategyService service;
private final StrategyConditionTimeframeService conditionTimeframes;
/** 전략 목록 조회 */
@GetMapping
@@ -49,6 +51,12 @@ public class StrategyController {
return ResponseEntity.ok(service.save(dto, parseUserId(userIdHeader), deviceId));
}
/** 전략 DSL에 포함된 평가 시간봉 목록 */
@GetMapping("/{id}/timeframes")
public ResponseEntity<List<String>> timeframes(@PathVariable Long id) {
return ResponseEntity.ok(conditionTimeframes.collectForStrategyList(id));
}
/** 전략 삭제 */
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
@@ -6,6 +6,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 실시간 전략 체크 설정 DTO.
* PUT /api/strategy/settings (요청/응답 공용)
@@ -29,10 +31,16 @@ public class LiveStrategySettingsDto {
/** "CANDLE_CLOSE" | "REALTIME_TICK" */
private String executionType;
/** 전략 평가 분봉: 1m, 3m, 5m, 15m, 30m, 1h, 4h, 1d */
/**
* @deprecated 전략 DSL의 START/TIMEFRAME에서 시간봉을 자동 추출합니다. 응답 전용 필드는 {@link #strategyCandleTypes}를 사용하세요.
*/
@Deprecated
@Builder.Default
private String candleType = "1m";
/** 전략 조건 DSL에 포함된 평가 시간봉 목록 (응답 전용) */
private List<String> strategyCandleTypes;
/** 매도 시그널 포지션 종속성 모드: "LONG_ONLY" | "SIGNAL_ONLY" */
@Builder.Default
private String positionMode = "LONG_ONLY";
@@ -55,6 +55,7 @@ public class LiveStrategyEvaluator {
private final IndicatorSettingsService indicatorSettingsService;
private final ObjectMapper objectMapper;
private final StrategySignalDeterminer determiner;
private final StrategyConditionTimeframeService conditionTimeframes;
/**
* Strategy 캐시: "market:candleType:strategyId" → Strategy
@@ -103,8 +104,7 @@ public class LiveStrategyEvaluator {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
if (!"CANDLE_CLOSE".equals(s.getExecutionType())) continue;
if (s.getStrategyId() == null) continue;
if (!candleType.equals(LiveStrategyTimeframeService.normalize(
s.getCandleType() != null ? s.getCandleType() : "1m"))) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), maturedIndex,
@@ -155,8 +155,7 @@ public class LiveStrategyEvaluator {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
if (s.getStrategyId() == null) continue;
if (!candleType.equals(LiveStrategyTimeframeService.normalize(
s.getCandleType() != null ? s.getCandleType() : "1m"))) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
// ★ Ta4j CachedIndicator 캐시 무효화:
// updateLastBar 로 provisional 봉 가격이 바뀌어도 CCI 등 CachedIndicator 는
@@ -202,8 +201,7 @@ public class LiveStrategyEvaluator {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
if (s.getStrategyId() == null) continue;
if (!candleType.equals(LiveStrategyTimeframeService.normalize(
s.getCandleType() != null ? s.getCandleType() : "1m"))) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
// CachedIndicator 캐시 무효화 — 확정봉의 최종 close 로 재계산
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
@@ -30,7 +30,7 @@ public class LiveStrategySettingsService {
private final AppSettingsService appSettingsService;
private final DynamicSubscriptionManager subscriptionManager;
private final LiveStrategyEvaluator liveStrategyEvaluator;
private final LiveStrategyTimeframeService timeframeService;
private final StrategyConditionTimeframeService conditionTimeframes;
// ── 조회 ──────────────────────────────────────────────────────────────────
@@ -42,7 +42,9 @@ public class LiveStrategySettingsService {
? toDto(entity.get())
: defaultDtoFromApp(market, app);
// 실시간 체크 마스터 스위치·전략 ID는 앱 전역 설정(gc_app_settings) 기준
return mergeGlobalTemplate(dto, app, market);
dto = mergeGlobalTemplate(dto, app, market);
enrichStrategyTimeframes(dto);
return dto;
}
@Transactional(readOnly = true)
@@ -52,21 +54,17 @@ public class LiveStrategySettingsService {
return List.of();
}
List<String> watchlist = listWatchlistSymbols(userId, deviceId);
Long strategyId = app.getLiveStrategyId();
List<String> strategyCandleTypes = conditionTimeframes.collectForStrategyList(strategyId);
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()
.map(m -> LiveStrategySettingsDto.builder()
.market(m)
.strategyId(app.getLiveStrategyId())
.strategyId(strategyId)
.liveCheck(true)
.executionType(app.getLiveExecutionType())
.positionMode(app.getLivePositionMode())
.candleType(ct)
.build();
})
.strategyCandleTypes(strategyCandleTypes)
.build())
.toList();
}
@@ -77,18 +75,12 @@ public class LiveStrategySettingsService {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
LiveStrategySettingsDto template = findEntity(userId, deviceId, market)
.map(e -> mergeGlobalTemplate(toDto(e), app, market))
.orElseGet(() -> {
LiveStrategySettingsDto dto = defaultDtoFromApp(market, app);
String inherited = resolveSharedCandleType(userId, deviceId);
if (inherited != null) {
dto.setCandleType(inherited);
}
return mergeGlobalTemplate(dto, 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 {} candleType={}", market, template.getCandleType());
log.debug("[LiveStrategy] watchlist add → enabled {}", market);
}
/** 관심종목 해제 시 — 해당 종목 전략 체크 비활성화 */
@@ -121,7 +113,6 @@ public class LiveStrategySettingsService {
.liveCheck(true)
.executionType(template.getExecutionType())
.positionMode(template.getPositionMode())
.candleType(template.getCandleType() != null ? template.getCandleType() : "1m")
.build();
results.add(upsertEntity(userId, deviceId, one));
}
@@ -148,9 +139,7 @@ public class LiveStrategySettingsService {
if (dto.getPositionMode() != null) {
out.setPositionMode(dto.getPositionMode());
}
if (dto.getCandleType() != null) {
out.setCandleType(LiveStrategyTimeframeService.normalize(dto.getCandleType()));
}
enrichStrategyTimeframes(out);
return out;
}
@@ -170,9 +159,6 @@ public class LiveStrategySettingsService {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
boolean active = Boolean.TRUE.equals(app.getLiveStrategyCheck())
&& app.getLiveStrategyId() != null;
String candleType = template != null && template.getCandleType() != null
? LiveStrategyTimeframeService.normalize(template.getCandleType())
: resolveSharedCandleType(userId, deviceId);
for (String symbol : listWatchlistSymbols(userId, deviceId)) {
LiveStrategySettingsDto row = findEntity(userId, deviceId, symbol)
.map(e -> mergeGlobalTemplate(toDto(e), app, symbol))
@@ -183,9 +169,7 @@ public class LiveStrategySettingsService {
? app.getLiveExecutionType() : row.getExecutionType());
row.setPositionMode(app.getLivePositionMode() != null
? app.getLivePositionMode() : row.getPositionMode());
if (candleType != null) {
row.setCandleType(candleType);
}
enrichStrategyTimeframes(row);
LiveStrategySettingsDto saved = upsertEntity(userId, deviceId, row);
if (active) {
pinIfReady(symbol, saved);
@@ -193,15 +177,21 @@ public class LiveStrategySettingsService {
}
}
/** 관심종목 중 이미 저장된 평가 분봉 (전역 템플릿 동기화용) */
private String resolveSharedCandleType(Long userId, String deviceId) {
for (String symbol : listWatchlistSymbols(userId, deviceId)) {
Optional<GcLiveStrategySettings> row = findEntity(userId, deviceId, symbol);
if (row.isPresent() && row.get().getCandleType() != null) {
return LiveStrategyTimeframeService.normalize(row.get().getCandleType());
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);
}
}
return null;
}
private LiveStrategySettingsDto upsertEntity(Long userId, String deviceId,
@@ -222,8 +212,6 @@ public class LiveStrategySettingsService {
"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);
@@ -232,12 +220,7 @@ public class LiveStrategySettingsService {
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, "1m");
if (!"1m".equals(ct)) {
subscriptionManager.ensureMarketPinned(market, ct);
}
pinAllStrategyTimeframes(market, dto.getStrategyId());
}
}
@@ -277,14 +260,15 @@ public class LiveStrategySettingsService {
}
private LiveStrategySettingsDto toDto(GcLiveStrategySettings e) {
return LiveStrategySettingsDto.builder()
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")
.candleType(e.getCandleType() != null ? e.getCandleType() : "1m")
.build();
enrichStrategyTimeframes(dto);
return dto;
}
/** 앱 전역 실시간 체크 ON/OFF·전략 ID를 DTO에 반영 (종목별 행과 분리) */
@@ -309,17 +293,6 @@ public class LiveStrategySettingsService {
.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";
};
}
}
@@ -0,0 +1,113 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.entity.GcStrategy;
import com.goldenchart.repository.GcStrategyRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 전략 DSL(buy/sell LogicNode)에 포함된 조건 판별 시간봉 수집.
* START / TIMEFRAME / AND|OR+TIMEFRAME 분기와 동일 규칙.
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class StrategyConditionTimeframeService {
private final GcStrategyRepository strategyRepo;
private final ObjectMapper objectMapper;
private final ConcurrentHashMap<Long, Set<String>> cache = new ConcurrentHashMap<>();
public Set<String> collectForStrategy(long strategyId) {
return cache.computeIfAbsent(strategyId, this::loadFromDb);
}
public List<String> collectForStrategyList(long strategyId) {
return List.copyOf(collectForStrategy(strategyId));
}
public boolean usesTimeframe(long strategyId, String candleType) {
String ct = LiveStrategyTimeframeService.normalize(candleType);
return collectForStrategy(strategyId).contains(ct);
}
public void invalidate(long strategyId) {
cache.remove(strategyId);
}
public void invalidateAll() {
cache.clear();
}
private Set<String> loadFromDb(long strategyId) {
Optional<GcStrategy> opt = strategyRepo.findById(strategyId);
if (opt.isEmpty()) return Set.of("1m");
GcStrategy strategy = opt.get();
Set<String> out = new LinkedHashSet<>();
collectFromJson(strategy.getBuyConditionJson(), out);
collectFromJson(strategy.getSellConditionJson(), out);
return out.isEmpty() ? Set.of("1m") : out;
}
private void collectFromJson(String json, Set<String> out) {
if (json == null || json.isBlank()) return;
try {
collectFromNode(objectMapper.readTree(json), out);
} catch (Exception e) {
log.warn("[StrategyTimeframes] JSON 파싱 실패: {}", e.getMessage());
out.add("1m");
}
}
private void collectFromNode(JsonNode node, Set<String> out) {
if (node == null || node.isNull()) return;
String type = node.path("type").asText("");
if ("TIMEFRAME".equals(type)) {
out.add(LiveStrategyTimeframeService.normalize(node.path("candleType").asText("1m")));
return;
}
if ("AND".equals(type) || "OR".equals(type)) {
JsonNode children = node.path("children");
if (children.isArray() && !children.isEmpty()) {
if (allTimeframeChildren(children)) {
for (JsonNode tf : children) {
out.add(LiveStrategyTimeframeService.normalize(tf.path("candleType").asText("1m")));
}
return;
}
for (JsonNode child : children) {
collectFromNode(child, out);
}
return;
}
}
JsonNode child = node.path("child");
if (!child.isMissingNode() && !child.isNull()) {
collectFromNode(child, out);
return;
}
// 단일 START(기본 1m) 또는 TIMEFRAME 래핑 없는 트리
out.add("1m");
}
private static boolean allTimeframeChildren(JsonNode children) {
for (JsonNode c : children) {
if (!"TIMEFRAME".equals(c.path("type").asText(""))) return false;
}
return true;
}
}
@@ -20,6 +20,7 @@ public class StrategyService {
private final GcStrategyRepository repository;
private final ObjectMapper objectMapper;
private final StrategyConditionTimeframeService conditionTimeframes;
// ── 조회 ──────────────────────────────────────────────────────────────────
@@ -61,13 +62,16 @@ public class StrategyService {
log.warn("전략 DSL 직렬화 실패: {}", e.getMessage());
}
return toDto(repository.save(entity));
GcStrategy saved = repository.save(entity);
conditionTimeframes.invalidate(saved.getId());
return toDto(saved);
}
// ── 삭제 ──────────────────────────────────────────────────────────────────
@Transactional
public void delete(Long id) {
conditionTimeframes.invalidate(id);
repository.deleteById(id);
}
@@ -5,6 +5,7 @@ import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
import com.goldenchart.service.LiveStrategyEvaluator;
import com.goldenchart.service.LiveStrategyTimeframeService;
import com.goldenchart.service.StrategyConditionTimeframeService;
import com.goldenchart.service.TradeSignalService;
import com.goldenchart.trading.pipeline.OrderExecutionQueue;
import com.goldenchart.storage.Ta4jStorage;
@@ -46,6 +47,7 @@ public class BarBuilder {
private final TradeSignalService tradeSignalService;
private final OrderExecutionQueue orderExecutionQueue;
private final GcLiveStrategySettingsRepository liveSettingsRepo;
private final StrategyConditionTimeframeService conditionTimeframes;
/** 상위 집계 타임프레임 (분 단위 집계 주기: 3, 5, 15, 30, 60, 240, 1440) */
private static final int[] UPPER_MINUTES = {3, 5, 15, 30, 60, 240, 1440};
@@ -155,12 +157,12 @@ public class BarBuilder {
}
}
/** 활성 실시간 전략 설정 중 해당 분봉(candleType)으로 평가하는 항목이 있는지 */
/** 활성 실시간 전략 설정 중 전략 DSL에 해당 분봉이 포함된 항목이 있는지 */
private boolean shouldEvaluateOnClose(String market, String candleType) {
String ct = LiveStrategyTimeframeService.normalize(candleType);
return liveSettingsRepo.findActiveByMarket(market).stream()
.anyMatch(s -> ct.equals(LiveStrategyTimeframeService.normalize(
s.getCandleType() != null ? s.getCandleType() : "1m")));
.filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck()) && s.getStrategyId() != null)
.anyMatch(s -> conditionTimeframes.usesTimeframe(s.getStrategyId(), ct));
}
/**
@@ -4,7 +4,7 @@ import com.goldenchart.dto.CandleBarDto;
import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
import com.goldenchart.service.LiveStrategyEvaluator;
import com.goldenchart.service.LiveStrategyTimeframeService;
import com.goldenchart.service.StrategyConditionTimeframeService;
import com.goldenchart.service.TradeSignalService;
import com.goldenchart.trading.pipeline.OrderExecutionQueue;
import com.goldenchart.storage.Ta4jStorage;
@@ -15,7 +15,6 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 3초 주기 라운드 로빈 스케줄러.
@@ -39,9 +38,9 @@ public class LiveStrategyScheduler {
private final LiveStrategyEvaluator evaluator;
private final Ta4jStorage ta4jStorage;
private final TradingWebSocketBroker broker;
private final BarBuilder barBuilder;
private final TradeSignalService tradeSignalService;
private final OrderExecutionQueue orderExecutionQueue;
private final StrategyConditionTimeframeService conditionTimeframes;
/**
* 3초마다 REALTIME_TICK 설정 종목을 순회하여 전략 판정 후 시그널 발행.
@@ -56,14 +55,19 @@ public class LiveStrategyScheduler {
if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
if (s.getStrategyId() == null) continue;
String market = s.getMarket();
String candleType = LiveStrategyTimeframeService.normalize(
s.getCandleType() != null ? s.getCandleType() : "1m");
String market = s.getMarket();
for (String candleType : conditionTimeframes.collectForStrategy(s.getStrategyId())) {
evaluateRealtimeTickForMarket(s, market, candleType);
}
}
}
if (!ta4jStorage.exists(market, candleType)) continue;
private void evaluateRealtimeTickForMarket(GcLiveStrategySettings s, String market,
String candleType) {
if (!ta4jStorage.exists(market, candleType)) return;
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
if (series.isEmpty()) continue;
if (series.isEmpty()) return;
String signal = evaluator.evaluateRealtimeTick(market, candleType);
if ("BUY".equals(signal) || "SELL".equals(signal)) {
@@ -105,6 +109,5 @@ public class LiveStrategyScheduler {
log.warn("[LiveStrategyScheduler] 시그널 DB 저장 실패: {}", ex.getMessage());
}
}
}
}
}