전략조건 상세설명 기능 추가
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user