지표탭 추가 수정

This commit is contained in:
Macbook
2026-05-27 13:16:02 +09:00
parent c72b987ff7
commit f22abbdc19
14 changed files with 505 additions and 76 deletions
@@ -1,7 +1,5 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.entity.GcStrategy;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
@@ -9,8 +7,9 @@ import com.goldenchart.repository.GcStrategyRepository;
import com.goldenchart.storage.Ta4jStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ta4j.core.*;
import org.ta4j.core.rules.BooleanRule;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseTradingRecord;
import org.ta4j.core.Rule;
import org.springframework.stereotype.Service;
@@ -51,17 +50,18 @@ public class LiveStrategyEvaluator {
private final GcLiveStrategySettingsRepository settingsRepo;
private final GcStrategyRepository strategyRepo;
private final Ta4jStorage ta4jStorage;
private final StrategyDslToTa4jAdapter adapter;
private final IndicatorSettingsService indicatorSettingsService;
private final ObjectMapper objectMapper;
private final StrategySignalDeterminer determiner;
private final StrategyConditionTimeframeService conditionTimeframes;
private final StrategyTriggerBranchEvaluator triggerBranchEvaluator;
private final StrategyBranchStateCache branchStateCache;
/**
* Strategy 캐시: "market:candleType:strategyId" → Strategy
* (entryRule / exitRule 포함)
* 트리거 분봉별 Rule 캐시: "market:candleType:strategyId" → (entryRule, exitRule)
*/
private final ConcurrentHashMap<String, Strategy> strategyCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TriggerRules> triggerRulesCache = new ConcurrentHashMap<>();
private record TriggerRules(Rule entryRule, Rule exitRule) {}
/**
* LONG_ONLY 모드용 TradingRecord 캐시: "market:candleType:strategyId" → TradingRecord
@@ -157,12 +157,9 @@ public class LiveStrategyEvaluator {
if (s.getStrategyId() == null) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
// ★ Ta4j CachedIndicator 캐시 무효화:
// updateLastBar 로 provisional 봉 가격이 바뀌어도 CCI 등 CachedIndicator 는
// 이미 캐시된 구 값을 반환한다. 전략을 재빌드하면 새 인디케이터 인스턴스를
// 생성(빈 캐시)하므로 getValue(currentIndex) 가 현재 가격으로 재계산된다.
// ★ Ta4j CachedIndicator 캐시 무효화 — Rule 트리 재빌드
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
strategyCache.remove(cacheKey);
triggerRulesCache.remove(cacheKey);
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), currentIndex,
@@ -205,7 +202,7 @@ public class LiveStrategyEvaluator {
// CachedIndicator 캐시 무효화 — 확정봉의 최종 close 로 재계산
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
strategyCache.remove(cacheKey);
triggerRulesCache.remove(cacheKey);
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), maturedIndex,
@@ -222,10 +219,20 @@ public class LiveStrategyEvaluator {
/** 설정 변경 시 해당 마켓 캐시 무효화 */
public void invalidateCache(String market) {
strategyCache.keySet().removeIf(k -> k.startsWith(market + ":"));
triggerRulesCache.keySet().removeIf(k -> k.startsWith(market + ":"));
tradingRecordCache.keySet().removeIf(k -> k.startsWith(market + ":"));
positionOpenCache.keySet().removeIf(k -> k.startsWith(market + ":"));
realtimeSignaledIdx.keySet().removeIf(k -> k.startsWith(market + ":"));
branchStateCache.invalidateMarket(market);
}
/** 전략 DSL 변경 시 Rule·분기 상태 캐시 무효화 */
public void invalidateStrategy(long strategyId) {
String suffix = ":" + strategyId;
triggerRulesCache.keySet().removeIf(k -> k.endsWith(suffix));
tradingRecordCache.keySet().removeIf(k -> k.endsWith(suffix));
positionOpenCache.keySet().removeIf(k -> k.endsWith(suffix));
branchStateCache.invalidateStrategy(strategyId);
}
// ── Private ───────────────────────────────────────────────────────────────
@@ -235,30 +242,27 @@ public class LiveStrategyEvaluator {
String deviceId, Long userId) {
String cacheKey = market + ":" + candleType + ":" + strategyId;
// computeIfAbsent 는 device context 전달이 불가하므 수동 분기
Strategy strategy = strategyCache.get(cacheKey);
if (strategy == null) {
strategy = buildStrategy(market, candleType, strategyId, deviceId, userId);
if (strategy != null) strategyCache.put(cacheKey, strategy);
TriggerRules rules = triggerRulesCache.get(cacheKey);
if (rules == null) {
rules = buildTriggerRules(market, candleType, strategyId, deviceId, userId);
if (rules == null) return "NONE";
triggerRulesCache.put(cacheKey, rules);
}
if (strategy == null) return "NONE";
try {
String mode = positionMode != null ? positionMode : "LONG_ONLY";
if ("SIGNAL_ONLY".equals(mode)) {
// 포지션 락 우회 — 순수 Rule 충족 여부만 판단
return determiner.determineSignal(strategy, null, index, "SIGNAL_ONLY");
return determiner.determineSignalFromRules(
rules.entryRule(), rules.exitRule(), null, index, "SIGNAL_ONLY");
}
// LONG_ONLY — TradingRecord 상태를 유지하여 포지션 검증
BaseTradingRecord record = tradingRecordCache.computeIfAbsent(
cacheKey, k -> new BaseTradingRecord());
boolean isOpen = Boolean.TRUE.equals(positionOpenCache.get(cacheKey));
String signal = determiner.determineSignal(strategy, record, index, "LONG_ONLY");
String signal = determiner.determineSignalFromRules(
rules.entryRule(), rules.exitRule(), record, index, "LONG_ONLY");
// 시그널에 따라 TradingRecord 갱신 및 포지션 상태 추적
if ("BUY".equals(signal) && !isOpen) {
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
org.ta4j.core.num.Num price = series.getBar(index).getClosePrice();
@@ -270,21 +274,20 @@ public class LiveStrategyEvaluator {
record.exit(index, price, series.numFactory().numOf(1));
positionOpenCache.put(cacheKey, false);
} else if ("BUY".equals(signal) || "SELL".equals(signal)) {
// 포지션 상태와 불일치 (이미 매수 중인데 또 BUY 등) → 신호 무시
signal = "NONE";
}
return signal;
} catch (Exception e) {
log.warn("[Evaluator] 전략 판정 오류 key={} idx={}: {}", cacheKey, index, e.getMessage());
strategyCache.remove(cacheKey);
triggerRulesCache.remove(cacheKey);
tradingRecordCache.remove(cacheKey);
}
return "NONE";
}
private Strategy buildStrategy(String market, String candleType, long strategyId,
String deviceId, Long userId) {
private TriggerRules buildTriggerRules(String market, String candleType, long strategyId,
String deviceId, Long userId) {
Optional<GcStrategy> strategyOpt = strategyRepo.findById(strategyId);
if (strategyOpt.isEmpty()) return null;
@@ -292,7 +295,6 @@ public class LiveStrategyEvaluator {
if (!ta4jStorage.exists(market, candleType)) return null;
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
// 사용자·장치별 지표 파라미터 우선 로드 (없으면 빈 맵 → adapter 기본값 사용)
Map<String, Map<String, Object>> indicatorParams =
indicatorSettingsService.getAll(userId, deviceId);
@@ -302,29 +304,18 @@ public class LiveStrategyEvaluator {
strategyId, market, barCount);
return null;
}
log.debug("[Evaluator] Strategy 빌드: strategyId={} market={} barCount={}", strategyId, market, barCount);
try {
Rule entryRule = buildRule(strategy.getBuyConditionJson(), series, indicatorParams, market);
Rule exitRule = buildRule(strategy.getSellConditionJson(), series, indicatorParams, market);
BaseStrategy builtStrategy = new BaseStrategy(entryRule, exitRule);
return builtStrategy;
Rule entryRule = triggerBranchEvaluator.buildTriggerRule(
strategy.getBuyConditionJson(), market, strategyId, "buy",
candleType, indicatorParams, ta4jStorage);
Rule exitRule = triggerBranchEvaluator.buildTriggerRule(
strategy.getSellConditionJson(), market, strategyId, "sell",
candleType, indicatorParams, ta4jStorage);
return new TriggerRules(entryRule, exitRule);
} catch (Exception e) {
log.error("[Evaluator] Strategy 빌드 실패 strategyId={}: {}", strategyId, e.getMessage());
log.error("[Evaluator] Trigger Rule 빌드 실패 strategyId={}: {}", strategyId, e.getMessage());
return null;
}
}
private Rule buildRule(String conditionJson, BarSeries series,
Map<String, Map<String, Object>> params,
String market) {
if (conditionJson == null || conditionJson.isBlank()) return new BooleanRule(false);
try {
JsonNode node = objectMapper.readTree(conditionJson);
return adapter.toRule(node, series, params, market, ta4jStorage);
} catch (Exception e) {
log.warn("[Evaluator] 조건 JSON 파싱 실패: {}", e.getMessage());
return new BooleanRule(false);
}
}
}