전략평가 로직 개선
This commit is contained in:
@@ -2,8 +2,10 @@ package com.goldenchart.service;
|
||||
|
||||
import com.goldenchart.entity.GcLiveStrategySettings;
|
||||
import com.goldenchart.entity.GcStrategy;
|
||||
import com.goldenchart.entity.GcTradeSignal;
|
||||
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
|
||||
import com.goldenchart.repository.GcStrategyRepository;
|
||||
import com.goldenchart.repository.GcTradeSignalRepository;
|
||||
import com.goldenchart.storage.Ta4jStorage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -57,6 +59,7 @@ public class LiveStrategyEvaluator {
|
||||
|
||||
private final GcLiveStrategySettingsRepository settingsRepo;
|
||||
private final GcStrategyRepository strategyRepo;
|
||||
private final GcTradeSignalRepository tradeSignalRepo;
|
||||
private final Ta4jStorage ta4jStorage;
|
||||
private final IndicatorSettingsService indicatorSettingsService;
|
||||
private final StrategySignalDeterminer determiner;
|
||||
@@ -79,6 +82,7 @@ public class LiveStrategyEvaluator {
|
||||
|
||||
/**
|
||||
* LONG_ONLY 모드용 포지션 오픈 여부 추적: Ta4j API 버전 차이 대응.
|
||||
* 서버 재시작 시 gc_trade_signal 최신 레코드로 복원된다 (resolvePositionOpen 참조).
|
||||
*/
|
||||
private final ConcurrentHashMap<String, Boolean> positionOpenCache = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -89,6 +93,24 @@ public class LiveStrategyEvaluator {
|
||||
*/
|
||||
private final ConcurrentHashMap<String, Integer> realtimeSignaledIdx = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* [항목1] REALTIME_TICK buildTriggerRules 오버헤드 경감용 앱-레벨 캐시.
|
||||
*
|
||||
* <p>전략 DSL(GcStrategy)과 지표 파라미터(IndicatorSettings)는 사용자가 편집하지 않는 한
|
||||
* 변경되지 않으므로 30초 TTL로 캐시하여 3초 주기 재조회 오버헤드를 줄인다.
|
||||
*/
|
||||
private final ConcurrentHashMap<Long, StrategyCacheEntry> strategyCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, IndicatorParamsCacheEntry> indicatorCache = new ConcurrentHashMap<>();
|
||||
|
||||
private record StrategyCacheEntry(GcStrategy strategy, long cachedAt) {}
|
||||
private record IndicatorParamsCacheEntry(
|
||||
Map<String, Map<String, Object>> params,
|
||||
Map<String, Map<String, Object>> visual,
|
||||
long cachedAt) {}
|
||||
|
||||
private static final long STRATEGY_CACHE_TTL_MS = 30_000L;
|
||||
private static final long INDICATOR_CACHE_TTL_MS = 30_000L;
|
||||
|
||||
// ── 공개 API ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -258,6 +280,14 @@ public class LiveStrategyEvaluator {
|
||||
tradingRecordCache.keySet().removeIf(k -> k.endsWith(suffix));
|
||||
positionOpenCache.keySet().removeIf(k -> k.endsWith(suffix));
|
||||
branchStateCache.invalidateStrategy(strategyId);
|
||||
// 앱 레벨 전략 DSL 캐시 무효화
|
||||
strategyCache.remove(strategyId);
|
||||
}
|
||||
|
||||
/** 지표 설정 변경 시 지표 파라미터 캐시 무효화 */
|
||||
public void invalidateIndicatorParams(Long userId, String deviceId) {
|
||||
String key = (userId != null ? userId : "") + ":" + (deviceId != null ? deviceId : "");
|
||||
indicatorCache.remove(key);
|
||||
}
|
||||
|
||||
// ── Private ───────────────────────────────────────────────────────────────
|
||||
@@ -308,7 +338,7 @@ public class LiveStrategyEvaluator {
|
||||
|
||||
BaseTradingRecord record = tradingRecordCache.computeIfAbsent(
|
||||
cacheKey, k -> new BaseTradingRecord());
|
||||
boolean isOpen = Boolean.TRUE.equals(positionOpenCache.get(cacheKey));
|
||||
boolean isOpen = resolvePositionOpen(cacheKey, market, strategyId);
|
||||
String signal = determiner.determineSignalFromRules(
|
||||
rules.entryRule(), rules.exitRule(), record, index, "LONG_ONLY");
|
||||
|
||||
@@ -337,17 +367,16 @@ public class LiveStrategyEvaluator {
|
||||
|
||||
private TriggerRules buildTriggerRules(String market, String candleType, long strategyId,
|
||||
String deviceId, Long userId, boolean useConfirmedOnly) {
|
||||
Optional<GcStrategy> strategyOpt = strategyRepo.findById(strategyId);
|
||||
if (strategyOpt.isEmpty()) return null;
|
||||
GcStrategy strategy = resolveStrategy(strategyId);
|
||||
if (strategy == null) return null;
|
||||
|
||||
GcStrategy strategy = strategyOpt.get();
|
||||
if (!ta4jStorage.exists(market, candleType)) return null;
|
||||
|
||||
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
|
||||
Map<String, Map<String, Object>> indicatorParams =
|
||||
indicatorSettingsService.getAll(userId, deviceId);
|
||||
Map<String, Map<String, Object>> indicatorVisual =
|
||||
indicatorSettingsService.getAllVisual(userId, deviceId);
|
||||
|
||||
IndicatorParamsCacheEntry indicatorEntry = resolveIndicatorParams(userId, deviceId);
|
||||
Map<String, Map<String, Object>> indicatorParams = indicatorEntry.params();
|
||||
Map<String, Map<String, Object>> indicatorVisual = indicatorEntry.visual();
|
||||
|
||||
int barCount = series.getBarCount();
|
||||
if (barCount < 2) {
|
||||
@@ -369,4 +398,68 @@ public class LiveStrategyEvaluator {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 항목1: 전략 DSL / 지표 파라미터 앱-레벨 캐시 헬퍼 ─────────────────────
|
||||
|
||||
/**
|
||||
* strategyId 로 GcStrategy 를 조회하되 TTL 내에는 캐시를 재사용한다.
|
||||
* 사용자가 전략을 수정하면 invalidateStrategy 로 캐시를 무효화한다.
|
||||
*/
|
||||
private GcStrategy resolveStrategy(long strategyId) {
|
||||
StrategyCacheEntry cached = strategyCache.get(strategyId);
|
||||
if (cached != null && (System.currentTimeMillis() - cached.cachedAt()) < STRATEGY_CACHE_TTL_MS) {
|
||||
return cached.strategy();
|
||||
}
|
||||
Optional<GcStrategy> opt = strategyRepo.findById(strategyId);
|
||||
if (opt.isEmpty()) {
|
||||
strategyCache.remove(strategyId);
|
||||
return null;
|
||||
}
|
||||
strategyCache.put(strategyId, new StrategyCacheEntry(opt.get(), System.currentTimeMillis()));
|
||||
return opt.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* (userId, deviceId) 키로 지표 파라미터 / visual 을 조회하되 TTL 내에는 캐시를 재사용한다.
|
||||
*/
|
||||
private IndicatorParamsCacheEntry resolveIndicatorParams(Long userId, String deviceId) {
|
||||
String key = (userId != null ? userId : "") + ":" + (deviceId != null ? deviceId : "");
|
||||
IndicatorParamsCacheEntry cached = indicatorCache.get(key);
|
||||
if (cached != null && (System.currentTimeMillis() - cached.cachedAt()) < INDICATOR_CACHE_TTL_MS) {
|
||||
return cached;
|
||||
}
|
||||
Map<String, Map<String, Object>> params = indicatorSettingsService.getAll(userId, deviceId);
|
||||
Map<String, Map<String, Object>> visual = indicatorSettingsService.getAllVisual(userId, deviceId);
|
||||
IndicatorParamsCacheEntry entry = new IndicatorParamsCacheEntry(params, visual, System.currentTimeMillis());
|
||||
indicatorCache.put(key, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
// ── 항목3: LONG_ONLY 포지션 상태 복원 (서버 재시작 중복 BUY 방지) ──────────
|
||||
|
||||
/**
|
||||
* 포지션 오픈 여부를 반환한다.
|
||||
*
|
||||
* <p>in-memory 캐시가 비어 있으면(서버 재시작 등) gc_trade_signal 테이블에서
|
||||
* 해당 전략×마켓의 가장 최근 시그널을 조회하여 포지션 상태를 복원한다.
|
||||
*
|
||||
* <ul>
|
||||
* <li>최신 시그널 = BUY → 포지션 오픈 (중복 BUY 차단)</li>
|
||||
* <li>최신 시그널 = SELL 또는 없음 → 포지션 없음</li>
|
||||
* </ul>
|
||||
*/
|
||||
private boolean resolvePositionOpen(String cacheKey, String market, long strategyId) {
|
||||
Boolean cached = positionOpenCache.get(cacheKey);
|
||||
if (cached != null) return cached;
|
||||
|
||||
// DB 복원: 해당 마켓×전략의 최신 시그널
|
||||
Optional<GcTradeSignal> latest =
|
||||
tradeSignalRepo.findFirstByMarketAndStrategyIdOrderByCreatedAtDesc(market, strategyId);
|
||||
boolean isOpen = latest.map(s -> "BUY".equals(s.getSignalType())).orElse(false);
|
||||
positionOpenCache.put(cacheKey, isOpen);
|
||||
if (isOpen) {
|
||||
log.info("[Evaluator] 포지션 상태 복원 (DB) key={} → OPEN (BUY 포지션 유지)", cacheKey);
|
||||
}
|
||||
return isOpen;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user