백엔드 전략 조건 평가 버그 3건 수정
1. evaluateOverallRule: 5m 전략 평가 시 1m 고정 시리즈 사용 버그 수정 - DSL에서 주 시간봉(TIMEFRAME 노드 candleType)을 추출하여 해당 시리즈 사용 - 없으면 1m 폴백 유지 2. buildConditionRule: kLength 등 지표 파라미터를 조건 노드 params에서 우선 읽도록 수정 - 조건 DSL의 params 필드가 있으면 전역 indicator settings보다 우선 적용 - 서버 DB에 indicator settings 없어도 전략 저장 당시 값 사용 가능 3. LiveConditionController: deviceId 헤더를 indicatorSettingsService에 전달 - x-device-id 헤더를 읽어 device 단위 지표 설정 조회 지원 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,7 @@ public class LiveConditionController {
|
||||
@RequestParam long strategyId,
|
||||
@RequestHeader Map<String, String> headers) {
|
||||
long uid = TradingControllerSupport.requireRegisteredUser(headers);
|
||||
return ResponseEntity.ok(service.evaluate(uid, null, market, strategyId));
|
||||
String deviceId = headers.get("x-device-id");
|
||||
return ResponseEntity.ok(service.evaluate(uid, deviceId, market, strategyId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,9 +174,15 @@ public class LiveConditionStatusService {
|
||||
objectMapper.readTree(conditionJson);
|
||||
if (dsl == null || dsl.isNull()) return null;
|
||||
|
||||
// 기준 시리즈: 1m 봉
|
||||
// DSL 루트의 주 시간봉 추출 (TIMEFRAME 노드가 있으면 해당 봉 사용)
|
||||
// 5m 전략에서 1m 시리즈로 평가하면 CROSS_UP 등이 부정확해지는 버그 수정
|
||||
String primaryTf = extractPrimaryTimeframe(dsl);
|
||||
if (!ta4jStorage.exists(market, primaryTf)) {
|
||||
// 지정 봉이 없으면 1m으로 폴백
|
||||
primaryTf = "1m";
|
||||
if (!ta4jStorage.exists(market, "1m")) return null;
|
||||
BarSeries series = ta4jStorage.getOrCreate(market, "1m");
|
||||
}
|
||||
BarSeries series = ta4jStorage.getOrCreate(market, primaryTf);
|
||||
if (series.isEmpty()) return null;
|
||||
|
||||
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
|
||||
@@ -190,6 +196,38 @@ public class LiveConditionStatusService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DSL 트리에서 주 시간봉을 추출한다.
|
||||
* TIMEFRAME 노드가 있으면 그 candleType을 반환, 없으면 "1m".
|
||||
* 복수의 TIMEFRAME이 있으면 가장 길게 등장하는 봉 우선 (다수결).
|
||||
*/
|
||||
private String extractPrimaryTimeframe(com.fasterxml.jackson.databind.JsonNode dsl) {
|
||||
Map<String, Integer> tfCount = new java.util.LinkedHashMap<>();
|
||||
collectTimeframes(dsl, tfCount);
|
||||
if (tfCount.isEmpty()) return "1m";
|
||||
return tfCount.entrySet().stream()
|
||||
.max(java.util.Map.Entry.comparingByValue())
|
||||
.map(java.util.Map.Entry::getKey)
|
||||
.orElse("1m");
|
||||
}
|
||||
|
||||
private void collectTimeframes(com.fasterxml.jackson.databind.JsonNode node,
|
||||
Map<String, Integer> out) {
|
||||
if (node == null || node.isNull()) return;
|
||||
String type = node.path("type").asText("CONDITION");
|
||||
if ("TIMEFRAME".equals(type)) {
|
||||
String tf = LiveStrategyTimeframeService.normalize(
|
||||
node.path("candleType").asText("1m"));
|
||||
out.merge(tf, 1, Integer::sum);
|
||||
}
|
||||
com.fasterxml.jackson.databind.JsonNode children = node.path("children");
|
||||
if (children.isArray()) {
|
||||
for (com.fasterxml.jackson.databind.JsonNode c : children) collectTimeframes(c, out);
|
||||
}
|
||||
com.fasterxml.jackson.databind.JsonNode child = node.path("child");
|
||||
if (!child.isMissingNode()) collectTimeframes(child, out);
|
||||
}
|
||||
|
||||
private void collectConditions(String json, String timeframe, String side,
|
||||
List<PendingCond> out) {
|
||||
if (json == null || json.isBlank()) return;
|
||||
|
||||
@@ -427,9 +427,14 @@ public class StrategyDslToTa4jAdapter {
|
||||
|
||||
// DSL 타입(STOCHASTIC 등) → DB 레지스트리 키(Stochastic 등) 변환 후 파라미터 조회
|
||||
String registryKey = toRegistryKey(indType);
|
||||
Map<String, Object> indParams = params != null
|
||||
Map<String, Object> globalParams = params != null
|
||||
? params.getOrDefault(registryKey, Map.of())
|
||||
: Map.of();
|
||||
// 조건 노드에 직접 저장된 params가 있으면 전역 설정보다 우선 적용
|
||||
// 예) {"kLength": 12, "smooth": 3} → 전역 Stochastic 설정(kLength=14)을 덮어씀
|
||||
Map<String, Object> condNodeParams = extractConditionNodeParams(cond);
|
||||
Map<String, Object> indParams = condNodeParams.isEmpty() ? globalParams
|
||||
: mergeParams(globalParams, condNodeParams);
|
||||
|
||||
try {
|
||||
Indicator<Num> left = resolveField(leftField, indType, indParams, series, condPeriod, leftPeriod, cond, ctx);
|
||||
@@ -1217,12 +1222,50 @@ public class StrategyDslToTa4jAdapter {
|
||||
private int intP(Map<String, Object> p, String k, int def) {
|
||||
if (p == null) return def;
|
||||
Object v = p.get(k);
|
||||
return v == null ? def : ((Number) v).intValue();
|
||||
if (v == null) return def;
|
||||
if (v instanceof Number n) return n.intValue();
|
||||
try { return Integer.parseInt(v.toString()); } catch (NumberFormatException e) { return def; }
|
||||
}
|
||||
|
||||
private double dblP(Map<String, Object> p, String k, double def) {
|
||||
if (p == null) return def;
|
||||
Object v = p.get(k);
|
||||
return v == null ? def : ((Number) v).doubleValue();
|
||||
if (v == null) return def;
|
||||
if (v instanceof Number n) return n.doubleValue();
|
||||
try { return Double.parseDouble(v.toString()); } catch (NumberFormatException e) { return def; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 노드 JSON에서 직접 저장된 파라미터를 추출한다.
|
||||
* 전략 편집기가 저장한 조건별 override params (예: {"kLength": 12}).
|
||||
* 없거나 객체가 아니면 빈 맵 반환.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> extractConditionNodeParams(JsonNode cond) {
|
||||
if (cond == null || cond.isNull()) return Map.of();
|
||||
JsonNode paramsNode = cond.path("params");
|
||||
if (paramsNode.isMissingNode() || paramsNode.isNull() || !paramsNode.isObject()) {
|
||||
return Map.of();
|
||||
}
|
||||
try {
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
paramsNode.fields().forEachRemaining(e -> {
|
||||
com.fasterxml.jackson.databind.JsonNode v = e.getValue();
|
||||
if (v.isNumber()) result.put(e.getKey(), v.numberValue());
|
||||
else if (v.isTextual()) result.put(e.getKey(), v.textValue());
|
||||
else if (v.isBoolean()) result.put(e.getKey(), v.booleanValue());
|
||||
});
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
/** globalParams 위에 overrides를 덮어쓴 새 맵 반환 */
|
||||
private Map<String, Object> mergeParams(Map<String, Object> global,
|
||||
Map<String, Object> overrides) {
|
||||
Map<String, Object> merged = new java.util.LinkedHashMap<>(global);
|
||||
merged.putAll(overrides);
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user