일목균형표 조건 수정
This commit is contained in:
@@ -51,6 +51,7 @@ public class LiveConditionStatusService {
|
|||||||
private final IndicatorSettingsService indicatorSettingsService;
|
private final IndicatorSettingsService indicatorSettingsService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final DynamicSubscriptionManager subscriptionManager;
|
private final DynamicSubscriptionManager subscriptionManager;
|
||||||
|
private final StrategyDslTimeframeNormalizer timeframeNormalizer;
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public LiveConditionStatusDto evaluate(Long userId, String deviceId,
|
public LiveConditionStatusDto evaluate(Long userId, String deviceId,
|
||||||
@@ -102,7 +103,7 @@ public class LiveConditionStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (chartBars != null && !chartBars.isEmpty() && chartTimeframe != null && !chartTimeframe.isBlank()) {
|
if (chartBars != null && !chartBars.isEmpty() && chartTimeframe != null && !chartTimeframe.isBlank()) {
|
||||||
return evaluateOnChartBars(strategy, market, strategyId, pending, chartBars, chartTimeframe,
|
return evaluateOnChartBars(strategy, market, strategyId, chartBars, chartTimeframe,
|
||||||
barTimeSec, params, visual);
|
barTimeSec, params, visual);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,19 +216,25 @@ public class LiveConditionStatusService {
|
|||||||
GcStrategy strategy,
|
GcStrategy strategy,
|
||||||
String market,
|
String market,
|
||||||
long strategyId,
|
long strategyId,
|
||||||
List<PendingCond> pending,
|
|
||||||
List<OhlcvBar> chartBars,
|
List<OhlcvBar> chartBars,
|
||||||
String chartTimeframe,
|
String chartTimeframe,
|
||||||
Long barTimeSec,
|
Long barTimeSec,
|
||||||
Map<String, Map<String, Object>> params,
|
Map<String, Map<String, Object>> params,
|
||||||
Map<String, Map<String, Object>> visual) {
|
Map<String, Map<String, Object>> visual) {
|
||||||
try {
|
try {
|
||||||
JsonNode buyDsl = objectMapper.readTree(
|
JsonNode buyDslRaw = objectMapper.readTree(
|
||||||
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null");
|
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null");
|
||||||
JsonNode sellDsl = objectMapper.readTree(
|
JsonNode sellDslRaw = objectMapper.readTree(
|
||||||
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null");
|
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null");
|
||||||
|
|
||||||
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
|
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
|
||||||
|
JsonNode buyDsl = timeframeNormalizer.remapForChartTimeframe(buyDslRaw, primaryTf);
|
||||||
|
JsonNode sellDsl = timeframeNormalizer.remapForChartTimeframe(sellDslRaw, primaryTf);
|
||||||
|
|
||||||
|
List<PendingCond> pending = new ArrayList<>();
|
||||||
|
collectConditionsFromNode(buyDsl, "1m", "buy", pending);
|
||||||
|
collectConditionsFromNode(sellDsl, "1m", "sell", pending);
|
||||||
|
|
||||||
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(chartBars, primaryTf);
|
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(chartBars, primaryTf);
|
||||||
if (primarySeries.isEmpty()) {
|
if (primarySeries.isEmpty()) {
|
||||||
return empty(market, strategyId);
|
return empty(market, strategyId);
|
||||||
@@ -322,16 +329,13 @@ public class LiveConditionStatusService {
|
|||||||
Long barTimeSec) {
|
Long barTimeSec) {
|
||||||
if (dsl == null || dsl.isNull()) return null;
|
if (dsl == null || dsl.isNull()) return null;
|
||||||
try {
|
try {
|
||||||
String primaryTf = extractPrimaryTimeframe(dsl);
|
if (primarySeries == null || primarySeries.isEmpty()) return null;
|
||||||
BarSeries series = resolveChartSeries(primaryTf, OhlcvBarSeriesSupport.normalizeTf(primaryTf),
|
|
||||||
primarySeries, seriesOverrides, market);
|
|
||||||
if (series == null || series.isEmpty()) return null;
|
|
||||||
|
|
||||||
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
|
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
|
||||||
new StrategyDslToTa4jAdapter.RuleBuildContext(
|
new StrategyDslToTa4jAdapter.RuleBuildContext(
|
||||||
series, params, visual, market, ta4jStorage, false, seriesOverrides);
|
primarySeries, params, visual, market, ta4jStorage, false, seriesOverrides);
|
||||||
Rule rule = adapter.toRule(dsl, ctx);
|
Rule rule = adapter.toRule(dsl, ctx);
|
||||||
int index = OhlcvBarSeriesSupport.resolveBarIndex(series, barTimeSec);
|
int index = OhlcvBarSeriesSupport.resolveBarIndex(primarySeries, barTimeSec);
|
||||||
return rule.isSatisfied(index, null);
|
return rule.isSatisfied(index, null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("[LiveCondition:chart] overall rule fail market={}: {}", market, e.getMessage());
|
log.debug("[LiveCondition:chart] overall rule fail market={}: {}", market, e.getMessage());
|
||||||
@@ -339,6 +343,12 @@ public class LiveConditionStatusService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void collectConditionsFromNode(JsonNode node, String timeframe, String side,
|
||||||
|
List<PendingCond> out) {
|
||||||
|
if (node == null || node.isNull()) return;
|
||||||
|
walk(node, timeframe, side, out, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
private LiveConditionStatusDto empty(String market, long strategyId) {
|
private LiveConditionStatusDto empty(String market, long strategyId) {
|
||||||
return LiveConditionStatusDto.builder()
|
return LiveConditionStatusDto.builder()
|
||||||
.market(market)
|
.market(market)
|
||||||
@@ -673,12 +683,14 @@ public class LiveConditionStatusService {
|
|||||||
indicatorSettingsService.getAllVisual(userId, deviceId);
|
indicatorSettingsService.getAllVisual(userId, deviceId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JsonNode buyDsl = objectMapper.readTree(
|
JsonNode buyDslRaw = objectMapper.readTree(
|
||||||
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null");
|
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null");
|
||||||
JsonNode sellDsl = objectMapper.readTree(
|
JsonNode sellDslRaw = objectMapper.readTree(
|
||||||
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null");
|
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null");
|
||||||
|
|
||||||
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
|
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
|
||||||
|
JsonNode buyDsl = timeframeNormalizer.remapForChartTimeframe(buyDslRaw, primaryTf);
|
||||||
|
JsonNode sellDsl = timeframeNormalizer.remapForChartTimeframe(sellDslRaw, primaryTf);
|
||||||
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(chartBars, primaryTf);
|
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(chartBars, primaryTf);
|
||||||
if (primarySeries.isEmpty()) {
|
if (primarySeries.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
|
|||||||
@@ -472,4 +472,126 @@ public class StrategyDslTimeframeNormalizer {
|
|||||||
private ObjectNode copyTimeframeWithCandleType(JsonNode tf, String candleType) {
|
private ObjectNode copyTimeframeWithCandleType(JsonNode tf, String candleType) {
|
||||||
return copyTimeframeWithCandleTypes(tf, Set.of(candleType));
|
return copyTimeframeWithCandleTypes(tf, Set.of(candleType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전략평가·차트 시그널 스캔 — UI에서 선택한 분봉으로 평가.
|
||||||
|
* 단일 평가 분봉 전략(예: 3m 저장)을 5m 차트에서 테스트할 때 TIMEFRAME·조건 candleType을 치환한다.
|
||||||
|
* 멀티 TF(3m+1h 등) 또는 START에 복수 candleTypes가 있으면 치환하지 않는다.
|
||||||
|
*/
|
||||||
|
public JsonNode remapForChartTimeframe(JsonNode root, String chartTimeframe) {
|
||||||
|
if (root == null || root.isNull() || chartTimeframe == null || chartTimeframe.isBlank()) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
String chartTf = LiveStrategyTimeframeService.normalize(chartTimeframe);
|
||||||
|
if (!canRemapSingleEvaluationTimeframe(root)) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
String strategyTf = resolveSingleEvaluationTimeframe(root);
|
||||||
|
if (chartTf.equals(strategyTf)) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
ObjectNode copy = (ObjectNode) root.deepCopy();
|
||||||
|
remapTimeframesRecursive(copy, strategyTf, chartTf);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String remapJsonForChartTimeframe(String json, String chartTimeframe) {
|
||||||
|
if (json == null || json.isBlank()) return json;
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(json);
|
||||||
|
JsonNode remapped = remapForChartTimeframe(root, chartTimeframe);
|
||||||
|
return objectMapper.writeValueAsString(remapped);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TIMEFRAME START에 복수 candleTypes 또는 AND/OR 아래 서로 다른 TF가 있으면 false */
|
||||||
|
private static boolean canRemapSingleEvaluationTimeframe(JsonNode root) {
|
||||||
|
if (root == null || root.isNull()) return false;
|
||||||
|
Set<String> tfScopes = new LinkedHashSet<>();
|
||||||
|
collectTimeframeScopeTypes(root, tfScopes);
|
||||||
|
if (tfScopes.size() > 1) return false;
|
||||||
|
return !hasMultiCandleTypesStart(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectTimeframeScopeTypes(JsonNode node, Set<String> out) {
|
||||||
|
if (node == null || node.isNull()) return;
|
||||||
|
String type = node.path("type").asText("");
|
||||||
|
if ("TIMEFRAME".equals(type)) {
|
||||||
|
if (hasMultiCandleTypesStart(node)) {
|
||||||
|
out.add("__multi__");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String ct = LiveStrategyTimeframeService.normalize(node.path("candleType").asText("1m"));
|
||||||
|
out.add(ct);
|
||||||
|
}
|
||||||
|
JsonNode children = node.path("children");
|
||||||
|
if (children.isArray()) {
|
||||||
|
for (JsonNode c : children) collectTimeframeScopeTypes(c, out);
|
||||||
|
}
|
||||||
|
JsonNode child = node.path("child");
|
||||||
|
if (!child.isMissingNode() && !child.isNull()) collectTimeframeScopeTypes(child, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasMultiCandleTypesStart(JsonNode tfNode) {
|
||||||
|
JsonNode arr = tfNode.path("candleTypes");
|
||||||
|
if (!arr.isArray() || arr.size() <= 1) return false;
|
||||||
|
Set<String> distinct = new LinkedHashSet<>();
|
||||||
|
for (JsonNode t : arr) {
|
||||||
|
distinct.add(LiveStrategyTimeframeService.normalize(t.asText("")));
|
||||||
|
}
|
||||||
|
return distinct.size() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveSingleEvaluationTimeframe(JsonNode root) {
|
||||||
|
Set<String> scopes = new LinkedHashSet<>();
|
||||||
|
collectTimeframeScopeTypes(root, scopes);
|
||||||
|
scopes.remove("__multi__");
|
||||||
|
if (scopes.size() == 1) return scopes.iterator().next();
|
||||||
|
Set<String> meaningful = collectMeaningfulCandleTypes(root);
|
||||||
|
if (meaningful.size() == 1) return meaningful.iterator().next();
|
||||||
|
return "1m";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void remapTimeframesRecursive(JsonNode node, String fromTf, String toTf) {
|
||||||
|
if (node == null || node.isNull() || !node.isObject()) return;
|
||||||
|
ObjectNode obj = (ObjectNode) node;
|
||||||
|
String type = obj.path("type").asText("");
|
||||||
|
|
||||||
|
if ("TIMEFRAME".equals(type)) {
|
||||||
|
obj.put("candleType", toTf);
|
||||||
|
if (obj.has("candleTypes")) {
|
||||||
|
ArrayNode arr = JsonNodeFactory.instance.arrayNode();
|
||||||
|
arr.add(toTf);
|
||||||
|
obj.set("candleTypes", arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("CONDITION".equals(type)) {
|
||||||
|
JsonNode cond = obj.path("condition");
|
||||||
|
if (cond.isObject()) {
|
||||||
|
remapConditionCandleType((ObjectNode) cond, "leftCandleType", fromTf, toTf);
|
||||||
|
remapConditionCandleType((ObjectNode) cond, "rightCandleType", fromTf, toTf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode children = obj.path("children");
|
||||||
|
if (children.isArray()) {
|
||||||
|
for (JsonNode c : children) remapTimeframesRecursive(c, fromTf, toTf);
|
||||||
|
}
|
||||||
|
JsonNode child = obj.path("child");
|
||||||
|
if (!child.isMissingNode() && !child.isNull()) remapTimeframesRecursive(child, fromTf, toTf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void remapConditionCandleType(ObjectNode cond, String field,
|
||||||
|
String fromTf, String toTf) {
|
||||||
|
if (!cond.has(field)) return;
|
||||||
|
String raw = cond.path(field).asText("");
|
||||||
|
if (raw.isBlank()) return;
|
||||||
|
String ct = LiveStrategyTimeframeService.normalize(raw);
|
||||||
|
if (fromTf.equals(ct)) {
|
||||||
|
cond.put(field, toTf);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,4 +259,39 @@ class StrategyDslTimeframeNormalizerTest {
|
|||||||
assertEquals("3m", root.path("candleTypes").get(1).asText());
|
assertEquals("3m", root.path("candleTypes").get(1).asText());
|
||||||
assertEquals("5m", root.path("candleTypes").get(2).asText());
|
assertEquals("5m", root.path("candleTypes").get(2).asText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void remapForChartTimeframe_single3mTo5m() throws Exception {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleType": "3m",
|
||||||
|
"children": [{
|
||||||
|
"type": "CONDITION",
|
||||||
|
"condition": {
|
||||||
|
"indicatorType": "ICHIMOKU",
|
||||||
|
"conditionType": "CLOUD_BREAK_UP"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
JsonNode remapped = normalizer.remapForChartTimeframe(objectMapper.readTree(json), "5m");
|
||||||
|
assertEquals("5m", remapped.path("candleType").asText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void remapForChartTimeframe_skipsMultiTimeframe() throws Exception {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"type": "AND",
|
||||||
|
"children": [
|
||||||
|
{ "type": "TIMEFRAME", "candleType": "3m", "children": [] },
|
||||||
|
{ "type": "TIMEFRAME", "candleType": "1h", "children": [] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
JsonNode root = objectMapper.readTree(json);
|
||||||
|
JsonNode remapped = normalizer.remapForChartTimeframe(root, "5m");
|
||||||
|
assertEquals("3m", remapped.path("children").get(0).path("candleType").asText());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function fetchEvaluationSnapshotAtBar(
|
|||||||
const baseRows = hydrated ? extractVirtualConditions(hydrated) : [];
|
const baseRows = hydrated ? extractVirtualConditions(hydrated) : [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pinStrategyEvaluationTimeframes(normalizedMarket, strategyId);
|
await pinStrategyEvaluationTimeframes(normalizedMarket, strategyId, chartTimeframe);
|
||||||
} catch {
|
} catch {
|
||||||
/* pin 실패해도 평가 시도 */
|
/* pin 실패해도 평가 시도 */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { loadStrategyTimeframes, pinCandleWatch, repairStrategyTimeframes, type StrategyDto } from './backendApi';
|
import { pinCandleWatch, loadStrategyTimeframes, repairStrategyTimeframes, type StrategyDto } from './backendApi';
|
||||||
import { normalizeStartCandleType } from './strategyStartNodes';
|
import { normalizeStartCandleType } from './strategyStartNodes';
|
||||||
import { collectUiEvaluationTimeframes, syncStrategyTimeframesFromLayoutIfNeeded } from './strategyTimeframeSync';
|
import { collectUiEvaluationTimeframes, syncStrategyTimeframesFromLayoutIfNeeded } from './strategyTimeframeSync';
|
||||||
|
|
||||||
/** 전략 DSL 평가 분봉 전체 pin — 1m 집계·상위 봉 warm-up */
|
/** 전략 DSL 평가 분봉 pin — chartTimeframe 이 있으면 UI 선택 분봉 우선 */
|
||||||
export async function pinStrategyEvaluationTimeframes(
|
export async function pinStrategyEvaluationTimeframes(
|
||||||
market: string,
|
market: string,
|
||||||
strategyId: number,
|
strategyId: number,
|
||||||
|
chartTimeframe?: string | null,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
|
if (chartTimeframe) {
|
||||||
|
const tf = normalizeStartCandleType(chartTimeframe);
|
||||||
|
await pinCandleWatch(market, tf);
|
||||||
|
return [tf];
|
||||||
|
}
|
||||||
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(strategyId);
|
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(strategyId);
|
||||||
if (!synced) return ['1m'];
|
if (!synced) return ['1m'];
|
||||||
await repairStrategyTimeframes(strategyId);
|
await repairStrategyTimeframes(strategyId);
|
||||||
|
|||||||
Reference in New Issue
Block a user