가상매매 수정
This commit is contained in:
@@ -31,6 +31,7 @@ public class LiveStrategySettingsService {
|
||||
private final DynamicSubscriptionManager subscriptionManager;
|
||||
private final LiveStrategyEvaluator liveStrategyEvaluator;
|
||||
private final StrategyConditionTimeframeService conditionTimeframes;
|
||||
private final StrategyService strategyService;
|
||||
|
||||
// ── 조회 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -235,6 +236,8 @@ public class LiveStrategySettingsService {
|
||||
|
||||
private void pinIfReady(String market, LiveStrategySettingsDto dto) {
|
||||
if (Boolean.TRUE.equals(dto.isLiveCheck()) && dto.getStrategyId() != null) {
|
||||
strategyService.repairTimeframeWrappers(dto.getStrategyId());
|
||||
conditionTimeframes.invalidate(dto.getStrategyId());
|
||||
pinAllStrategyTimeframes(market, dto.getStrategyId());
|
||||
}
|
||||
}
|
||||
|
||||
+76
-18
@@ -4,8 +4,8 @@ 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.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -19,15 +19,27 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* START / TIMEFRAME / AND|OR+TIMEFRAME 분기와 동일 규칙.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class StrategyConditionTimeframeService {
|
||||
|
||||
private final GcStrategyRepository strategyRepo;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final StrategyDslTimeframeNormalizer dslNormalizer;
|
||||
private final LiveStrategyEvaluator liveStrategyEvaluator;
|
||||
|
||||
private final ConcurrentHashMap<Long, Set<String>> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public StrategyConditionTimeframeService(
|
||||
GcStrategyRepository strategyRepo,
|
||||
ObjectMapper objectMapper,
|
||||
StrategyDslTimeframeNormalizer dslNormalizer,
|
||||
@Lazy LiveStrategyEvaluator liveStrategyEvaluator) {
|
||||
this.strategyRepo = strategyRepo;
|
||||
this.objectMapper = objectMapper;
|
||||
this.dslNormalizer = dslNormalizer;
|
||||
this.liveStrategyEvaluator = liveStrategyEvaluator;
|
||||
}
|
||||
|
||||
public Set<String> collectForStrategy(long strategyId) {
|
||||
return cache.computeIfAbsent(strategyId, this::loadFromDb);
|
||||
}
|
||||
@@ -53,33 +65,79 @@ public class StrategyConditionTimeframeService {
|
||||
Optional<GcStrategy> opt = strategyRepo.findById(strategyId);
|
||||
if (opt.isEmpty()) return Set.of("1m");
|
||||
GcStrategy strategy = opt.get();
|
||||
ensureDslRepaired(strategy);
|
||||
|
||||
Set<String> out = new LinkedHashSet<>();
|
||||
collectFromJson(strategy.getBuyConditionJson(), out);
|
||||
collectFromJson(strategy.getSellConditionJson(), out);
|
||||
return out.isEmpty() ? Set.of("1m") : out;
|
||||
collectFromJson(strategy.getBuyConditionJson(), out, strategy.getName());
|
||||
collectFromJson(strategy.getSellConditionJson(), out, strategy.getName());
|
||||
|
||||
if (out.isEmpty()) {
|
||||
String fromName = StrategyDslTimeframeNormalizer.inferFromStrategyName(strategy.getName());
|
||||
return Set.of(fromName != null ? fromName : "1m");
|
||||
}
|
||||
return 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");
|
||||
/** TIMEFRAME 누락·잘못된 1m 래핑을 전략명·DSL 기준으로 DB 보정 */
|
||||
private void ensureDslRepaired(GcStrategy strategy) {
|
||||
boolean changed = false;
|
||||
String name = strategy.getName();
|
||||
|
||||
if (strategy.getBuyConditionJson() != null && !strategy.getBuyConditionJson().isBlank()) {
|
||||
String fixed = dslNormalizer.normalizeJson(strategy.getBuyConditionJson(), name);
|
||||
if (!fixed.equals(strategy.getBuyConditionJson())) {
|
||||
strategy.setBuyConditionJson(fixed);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (strategy.getSellConditionJson() != null && !strategy.getSellConditionJson().isBlank()) {
|
||||
String fixed = dslNormalizer.normalizeJson(strategy.getSellConditionJson(), name);
|
||||
if (!fixed.equals(strategy.getSellConditionJson())) {
|
||||
strategy.setSellConditionJson(fixed);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
strategyRepo.save(strategy);
|
||||
liveStrategyEvaluator.invalidateStrategy(strategy.getId());
|
||||
log.info("[StrategyTimeframes] DSL 분봉 보정 strategyId={} name={}",
|
||||
strategy.getId(), name);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectFromNode(JsonNode node, Set<String> out) {
|
||||
private void collectFromJson(String json, Set<String> out, String strategyName) {
|
||||
if (json == null || json.isBlank()) return;
|
||||
try {
|
||||
collectFromNode(objectMapper.readTree(json), out, strategyName);
|
||||
} catch (Exception e) {
|
||||
log.warn("[StrategyTimeframes] JSON 파싱 실패: {}", e.getMessage());
|
||||
addFallbackTimeframe(out, strategyName);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectFromNode(JsonNode node, Set<String> out, String strategyName) {
|
||||
if (node == null || node.isNull()) return;
|
||||
if (collectTimeframesFromNode(node, out)) return;
|
||||
|
||||
Set<String> explicit = StrategyDslTimeframeNormalizer.collectExplicitCandleTypes(node);
|
||||
if (!explicit.isEmpty()) {
|
||||
out.addAll(explicit);
|
||||
Set<String> meaningful = StrategyDslTimeframeNormalizer.collectMeaningfulCandleTypes(node);
|
||||
if (!meaningful.isEmpty()) {
|
||||
out.addAll(meaningful);
|
||||
return;
|
||||
}
|
||||
// TIMEFRAME·명시 분봉 없음 — 기본 1m
|
||||
out.add("1m");
|
||||
|
||||
String fromName = StrategyDslTimeframeNormalizer.inferFromStrategyName(strategyName);
|
||||
if (fromName != null) {
|
||||
out.add(fromName);
|
||||
return;
|
||||
}
|
||||
|
||||
addFallbackTimeframe(out, strategyName);
|
||||
}
|
||||
|
||||
private void addFallbackTimeframe(Set<String> out, String strategyName) {
|
||||
String fromName = StrategyDslTimeframeNormalizer.inferFromStrategyName(strategyName);
|
||||
out.add(fromName != null ? fromName : "1m");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.stereotype.Component;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 전략 DSL에 TIMEFRAME 래핑이 없으면 평가 분봉을 추론해 래핑한다.
|
||||
@@ -19,13 +20,36 @@ import java.util.UUID;
|
||||
@RequiredArgsConstructor
|
||||
public class StrategyDslTimeframeNormalizer {
|
||||
|
||||
private static final Pattern NAME_3M = Pattern.compile("3\\s*분|3m", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NAME_5M = Pattern.compile("5\\s*분|5m", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NAME_10M = Pattern.compile("10\\s*분|10m", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NAME_15M = Pattern.compile("15\\s*분|15m", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NAME_30M = Pattern.compile("30\\s*분|30m", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NAME_1H = Pattern.compile("1\\s*시간|1h", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NAME_4H = Pattern.compile("4\\s*시간|4h", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern NAME_1D = Pattern.compile("일봉|1\\s*일|1d", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public JsonNode normalize(JsonNode root) {
|
||||
public JsonNode normalize(JsonNode root) {
|
||||
return normalize(root, null);
|
||||
}
|
||||
|
||||
public JsonNode normalize(JsonNode root, String strategyName) {
|
||||
if (root == null || root.isNull()) return root;
|
||||
|
||||
if ("TIMEFRAME".equals(root.path("type").asText(""))) {
|
||||
String expected = inferPrimaryCandleType(root, strategyName);
|
||||
String current = LiveStrategyTimeframeService.normalize(root.path("candleType").asText("1m"));
|
||||
if (!expected.equals(current)) {
|
||||
return copyTimeframeWithCandleType(root, expected);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
if (hasTimeframeScope(root)) return root;
|
||||
|
||||
String primary = inferPrimaryCandleType(root);
|
||||
String primary = inferPrimaryCandleType(root, strategyName);
|
||||
ObjectNode wrapped = objectMapper.createObjectNode();
|
||||
wrapped.put("id", UUID.randomUUID().toString());
|
||||
wrapped.put("type", "TIMEFRAME");
|
||||
@@ -37,16 +61,34 @@ public class StrategyDslTimeframeNormalizer {
|
||||
}
|
||||
|
||||
public String normalizeJson(String json) {
|
||||
return normalizeJson(json, null);
|
||||
}
|
||||
|
||||
public String normalizeJson(String json, String strategyName) {
|
||||
if (json == null || json.isBlank()) return json;
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(json);
|
||||
JsonNode normalized = normalize(root);
|
||||
JsonNode normalized = normalize(root, strategyName);
|
||||
return objectMapper.writeValueAsString(normalized);
|
||||
} catch (Exception e) {
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
/** 전략 이름에서 평가 분봉 추론 (예: cci_3분봉 테스트 → 3m) */
|
||||
public static String inferFromStrategyName(String name) {
|
||||
if (name == null || name.isBlank()) return null;
|
||||
if (NAME_3M.matcher(name).find()) return "3m";
|
||||
if (NAME_5M.matcher(name).find()) return "5m";
|
||||
if (NAME_10M.matcher(name).find()) return "10m";
|
||||
if (NAME_15M.matcher(name).find()) return "15m";
|
||||
if (NAME_30M.matcher(name).find()) return "30m";
|
||||
if (NAME_4H.matcher(name).find()) return "4h";
|
||||
if (NAME_1H.matcher(name).find()) return "1h";
|
||||
if (NAME_1D.matcher(name).find()) return "1d";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 루트 TIMEFRAME 또는 AND|OR + 전부 TIMEFRAME 자식 */
|
||||
public static boolean hasTimeframeScope(JsonNode node) {
|
||||
if (node == null || node.isNull()) return false;
|
||||
@@ -88,6 +130,16 @@ public class StrategyDslTimeframeNormalizer {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 조건 기본값 1m 은 제외하고 의미 있는 분봉만 */
|
||||
public static Set<String> collectMeaningfulCandleTypes(JsonNode node) {
|
||||
Set<String> all = collectExplicitCandleTypes(node);
|
||||
Set<String> meaningful = new LinkedHashSet<>();
|
||||
for (String ct : all) {
|
||||
if (!"1m".equals(ct)) meaningful.add(ct);
|
||||
}
|
||||
return meaningful;
|
||||
}
|
||||
|
||||
private static void collectExplicitCandleTypes(JsonNode node, Set<String> out) {
|
||||
if (node == null || node.isNull()) return;
|
||||
|
||||
@@ -117,21 +169,33 @@ public class StrategyDslTimeframeNormalizer {
|
||||
}
|
||||
|
||||
public static String inferPrimaryCandleType(JsonNode node) {
|
||||
return inferPrimaryCandleType(node, null);
|
||||
}
|
||||
|
||||
public static String inferPrimaryCandleType(JsonNode node, String strategyName) {
|
||||
JsonNode nested = findNestedTimeframe(node);
|
||||
if (nested != null) {
|
||||
return LiveStrategyTimeframeService.normalize(nested.path("candleType").asText("1m"));
|
||||
String ct = LiveStrategyTimeframeService.normalize(nested.path("candleType").asText("1m"));
|
||||
if (!"1m".equals(ct)) return ct;
|
||||
String fromName = inferFromStrategyName(strategyName);
|
||||
if (fromName != null) return fromName;
|
||||
return ct;
|
||||
}
|
||||
|
||||
Set<String> meaningful = collectMeaningfulCandleTypes(node);
|
||||
if (!meaningful.isEmpty()) return meaningful.iterator().next();
|
||||
|
||||
String fromName = inferFromStrategyName(strategyName);
|
||||
if (fromName != null) return fromName;
|
||||
|
||||
Set<String> explicit = collectExplicitCandleTypes(node);
|
||||
if (explicit.isEmpty()) return "1m";
|
||||
|
||||
Set<String> non1m = new LinkedHashSet<>();
|
||||
for (String ct : explicit) {
|
||||
if (!"1m".equals(ct)) non1m.add(ct);
|
||||
}
|
||||
if (non1m.size() == 1) return non1m.iterator().next();
|
||||
if (non1m.size() > 1) return non1m.iterator().next();
|
||||
if (explicit.size() == 1) return explicit.iterator().next();
|
||||
return explicit.iterator().next();
|
||||
return "1m";
|
||||
}
|
||||
|
||||
private ObjectNode copyTimeframeWithCandleType(JsonNode tf, String candleType) {
|
||||
ObjectNode copy = tf.deepCopy();
|
||||
copy.put("candleType", LiveStrategyTimeframeService.normalize(candleType));
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,13 +56,16 @@ public class StrategyService {
|
||||
else if (entity.getEnabled() == null) entity.setEnabled(true);
|
||||
|
||||
try {
|
||||
String strategyName = entity.getName() != null ? entity.getName() : dto.getName();
|
||||
entity.setBuyConditionJson(
|
||||
dto.getBuyCondition() != null
|
||||
? dslTimeframeNormalizer.normalizeJson(objectMapper.writeValueAsString(dto.getBuyCondition()))
|
||||
? dslTimeframeNormalizer.normalizeJson(
|
||||
objectMapper.writeValueAsString(dto.getBuyCondition()), strategyName)
|
||||
: null);
|
||||
entity.setSellConditionJson(
|
||||
dto.getSellCondition() != null
|
||||
? dslTimeframeNormalizer.normalizeJson(objectMapper.writeValueAsString(dto.getSellCondition()))
|
||||
? dslTimeframeNormalizer.normalizeJson(
|
||||
objectMapper.writeValueAsString(dto.getSellCondition()), strategyName)
|
||||
: null);
|
||||
} catch (Exception e) {
|
||||
log.warn("전략 DSL 직렬화 실패: {}", e.getMessage());
|
||||
@@ -83,14 +86,16 @@ public class StrategyService {
|
||||
return repository.findById(id).map(entity -> {
|
||||
boolean changed = false;
|
||||
if (entity.getBuyConditionJson() != null && !entity.getBuyConditionJson().isBlank()) {
|
||||
String fixed = dslTimeframeNormalizer.normalizeJson(entity.getBuyConditionJson());
|
||||
String fixed = dslTimeframeNormalizer.normalizeJson(
|
||||
entity.getBuyConditionJson(), entity.getName());
|
||||
if (!fixed.equals(entity.getBuyConditionJson())) {
|
||||
entity.setBuyConditionJson(fixed);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (entity.getSellConditionJson() != null && !entity.getSellConditionJson().isBlank()) {
|
||||
String fixed = dslTimeframeNormalizer.normalizeJson(entity.getSellConditionJson());
|
||||
String fixed = dslTimeframeNormalizer.normalizeJson(
|
||||
entity.getSellConditionJson(), entity.getName());
|
||||
if (!fixed.equals(entity.getSellConditionJson())) {
|
||||
entity.setSellConditionJson(fixed);
|
||||
changed = true;
|
||||
|
||||
Reference in New Issue
Block a user