전략시간봉 오류 재수정

This commit is contained in:
Macbook
2026-05-28 02:09:38 +09:00
parent 5f9b20d907
commit 9137864f48
8 changed files with 215 additions and 90 deletions
@@ -101,23 +101,33 @@ public class LiveStrategyEvaluator {
ta4jStorage.getOrCreate(market, candleType).getBarCount());
for (GcLiveStrategySettings s : settings) {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
if (!"CANDLE_CLOSE".equals(s.getExecutionType())) continue;
if (s.getStrategyId() == null) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), maturedIndex,
s.getDeviceId(), s.getUserId());
if (!"NONE".equals(result)) {
log.info("[Evaluator] CANDLE_CLOSE {} {} idx={} mode={} → {}",
market, candleType, maturedIndex, s.getPositionMode(), result);
return result;
}
String result = evaluateSettingOnCandleClose(s, market, candleType, maturedIndex);
if (!"NONE".equals(result)) return result;
}
return "NONE";
}
/**
* 단일 live 설정 — 봉 마감(CANDLE_CLOSE) 평가.
*/
public String evaluateSettingOnCandleClose(GcLiveStrategySettings s, String market,
String candleType, int maturedIndex) {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) return "NONE";
if (!"CANDLE_CLOSE".equals(s.getExecutionType())) return "NONE";
if (s.getStrategyId() == null) return "NONE";
if (!ta4jStorage.exists(market, candleType)) return "NONE";
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), maturedIndex,
s.getDeviceId(), s.getUserId());
if (!"NONE".equals(result)) {
log.info("[Evaluator] CANDLE_CLOSE strategyId={} {} {} idx={} mode={} → {}",
s.getStrategyId(), market, candleType, maturedIndex, s.getPositionMode(), result);
}
return result;
}
/**
* 방식 B — 3초 스케줄러에서 호출.
*
@@ -141,37 +151,44 @@ public class LiveStrategyEvaluator {
List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
if (settings.isEmpty()) return "NONE";
for (GcLiveStrategySettings s : settings) {
String result = evaluateSettingRealtimeTick(s, market, candleType);
if (!"NONE".equals(result)) return result;
}
return "NONE";
}
/**
* 단일 live 설정 — 3초 스케줄러(REALTIME_TICK) 평가.
*/
public String evaluateSettingRealtimeTick(GcLiveStrategySettings s, String market,
String candleType) {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) return "NONE";
if (!"REALTIME_TICK".equals(s.getExecutionType())) return "NONE";
if (s.getStrategyId() == null) return "NONE";
if (!ta4jStorage.exists(market, candleType)) return "NONE";
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
if (series.isEmpty()) return "NONE";
int currentIndex = series.getEndIndex();
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
// 동일 provisional 봉에서 이미 시그널을 냈으면 중복 발화 방지
String dedupeKey = market + ":" + candleType;
String dedupeKey = market + ":" + candleType + ":" + s.getStrategyId();
Integer lastSignaledIdx = realtimeSignaledIdx.get(dedupeKey);
if (lastSignaledIdx != null && lastSignaledIdx == currentIndex) return "NONE";
for (GcLiveStrategySettings s : settings) {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
if (s.getStrategyId() == null) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
triggerRulesCache.remove(cacheKey);
// ★ Ta4j CachedIndicator 캐시 무효화 — Rule 트리 재빌드
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
triggerRulesCache.remove(cacheKey);
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), currentIndex,
s.getDeviceId(), s.getUserId());
if (!"NONE".equals(result)) {
log.info("[Evaluator] REALTIME_TICK {} {} idx={} mode={} → {}",
market, candleType, currentIndex, s.getPositionMode(), result);
realtimeSignaledIdx.put(dedupeKey, currentIndex);
return result;
}
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), currentIndex,
s.getDeviceId(), s.getUserId());
if (!"NONE".equals(result)) {
log.info("[Evaluator] REALTIME_TICK strategyId={} {} {} idx={} mode={} → {}",
s.getStrategyId(), market, candleType, currentIndex, s.getPositionMode(), result);
realtimeSignaledIdx.put(dedupeKey, currentIndex);
}
return "NONE";
return result;
}
/**
@@ -192,31 +209,39 @@ public class LiveStrategyEvaluator {
public String evaluateRealtimeAtClose(String market, String candleType, int maturedIndex) {
List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
if (settings.isEmpty()) return "NONE";
if (!ta4jStorage.exists(market, candleType)) return "NONE";
for (GcLiveStrategySettings s : settings) {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
if (s.getStrategyId() == null) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
// CachedIndicator 캐시 무효화 — 확정봉의 최종 close 로 재계산
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
triggerRulesCache.remove(cacheKey);
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), maturedIndex,
s.getDeviceId(), s.getUserId());
if (!"NONE".equals(result)) {
log.info("[Evaluator] REALTIME_TICK @candle_close {} {} idx={} mode={} → {}",
market, candleType, maturedIndex, s.getPositionMode(), result);
realtimeSignaledIdx.put(market + ":" + candleType, maturedIndex);
return result;
}
String result = evaluateSettingRealtimeAtClose(s, market, candleType, maturedIndex);
if (!"NONE".equals(result)) return result;
}
return "NONE";
}
/**
* 단일 live 설정 — 봉 마감 시점 REALTIME_TICK 평가(교차 누락 방지).
*/
public String evaluateSettingRealtimeAtClose(GcLiveStrategySettings s, String market,
String candleType, int maturedIndex) {
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) return "NONE";
if (!"REALTIME_TICK".equals(s.getExecutionType())) return "NONE";
if (s.getStrategyId() == null) return "NONE";
if (!ta4jStorage.exists(market, candleType)) return "NONE";
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
triggerRulesCache.remove(cacheKey);
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), maturedIndex,
s.getDeviceId(), s.getUserId());
if (!"NONE".equals(result)) {
log.info("[Evaluator] REALTIME_TICK @candle_close strategyId={} {} {} idx={} mode={} → {}",
s.getStrategyId(), market, candleType, maturedIndex, s.getPositionMode(), result);
realtimeSignaledIdx.put(market + ":" + candleType + ":" + s.getStrategyId(), maturedIndex);
}
return result;
}
/** 설정 변경 시 해당 마켓 캐시 무효화 */
public void invalidateCache(String market) {
triggerRulesCache.keySet().removeIf(k -> k.startsWith(market + ":"));
@@ -170,40 +170,49 @@ public class BarBuilder {
*/
private void evaluateStrategyOnBarClose(String market, String candleType, int maturedIndex,
Bar signalBar) {
String closeSignal = liveStrategyEvaluator.evaluateCandleClose(market, candleType, maturedIndex);
String signalExecType = "CANDLE_CLOSE";
List<GcLiveStrategySettings> activeSettings = liveSettingsRepo.findActiveByMarket(market).stream()
.filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck()) && s.getStrategyId() != null)
.toList();
if (activeSettings.isEmpty()) return;
if ("NONE".equals(closeSignal)) {
closeSignal = liveStrategyEvaluator.evaluateRealtimeAtClose(market, candleType, maturedIndex);
signalExecType = "REALTIME_TICK";
}
long candleTimeEpoch = signalBar.getEndTime().getEpochSecond()
- signalBar.getTimePeriod().getSeconds();
if ("BUY".equals(closeSignal) || "SELL".equals(closeSignal)) {
publishStrategySignal(market, candleType, signalBar, closeSignal);
}
for (GcLiveStrategySettings s : activeSettings) {
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) return;
String closeSignal = "NONE";
String signalExecType = null;
try {
final String execType = signalExecType;
GcLiveStrategySettings activeSetting = liveSettingsRepo.findActiveByMarket(market)
.stream().filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck())
&& execType.equals(s.getExecutionType())).findFirst().orElse(null);
long candleTimeEpoch = signalBar.getEndTime().getEpochSecond()
- signalBar.getTimePeriod().getSeconds();
String devId = activeSetting != null ? activeSetting.getDeviceId() : null;
Long uid = activeSetting != null ? activeSetting.getUserId() : null;
Long stratId = activeSetting != null ? activeSetting.getStrategyId() : null;
tradeSignalService.save(
devId, uid, market, stratId, null,
closeSignal, signalBar.getClosePrice().doubleValue(),
candleTimeEpoch, candleType, execType);
if (devId != null) {
orderExecutionQueue.submitSignal(devId, uid, market, stratId, closeSignal,
signalBar.getClosePrice().doubleValue());
if ("CANDLE_CLOSE".equals(s.getExecutionType())) {
closeSignal = liveStrategyEvaluator.evaluateSettingOnCandleClose(
s, market, candleType, maturedIndex);
signalExecType = "CANDLE_CLOSE";
} else if ("REALTIME_TICK".equals(s.getExecutionType())) {
closeSignal = liveStrategyEvaluator.evaluateSettingRealtimeAtClose(
s, market, candleType, maturedIndex);
signalExecType = "REALTIME_TICK";
}
if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) continue;
publishStrategySignal(market, candleType, signalBar, closeSignal);
try {
tradeSignalService.save(
s.getDeviceId(), s.getUserId(),
market, s.getStrategyId(), null,
closeSignal, signalBar.getClosePrice().doubleValue(),
candleTimeEpoch, candleType, signalExecType);
if (s.getDeviceId() != null) {
orderExecutionQueue.submitSignal(
s.getDeviceId(), s.getUserId(), market,
s.getStrategyId(), closeSignal,
signalBar.getClosePrice().doubleValue());
}
} catch (Exception e) {
log.warn("[BarBuilder] 시그널 처리 실패 ({}/{} strategyId={}): {}",
market, candleType, s.getStrategyId(), e.getMessage());
}
} catch (Exception e) {
log.warn("[BarBuilder] 시그널 처리 실패 ({}/{}): {}", market, candleType, e.getMessage());
}
}
@@ -69,7 +69,7 @@ public class LiveStrategyScheduler {
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
if (series.isEmpty()) return;
String signal = evaluator.evaluateRealtimeTick(market, candleType);
String signal = evaluator.evaluateSettingRealtimeTick(s, market, candleType);
if ("BUY".equals(signal) || "SELL".equals(signal)) {
// 현재 진행 중인 캔들 정보 조회
org.ta4j.core.Bar lastBar = series.getLastBar();