전략평가 메뉴 추가

This commit is contained in:
Macbook
2026-06-12 14:39:17 +09:00
parent cb1bde2563
commit ae9266bd28
23 changed files with 1977 additions and 19 deletions
@@ -53,6 +53,21 @@ public class LiveConditionStatusService {
@Transactional(readOnly = true)
public LiveConditionStatusDto evaluate(Long userId, String deviceId,
String market, long strategyId) {
return evaluate(userId, deviceId, market, strategyId, null);
}
@Transactional(readOnly = true)
public LiveConditionStatusDto evaluate(Long userId, String deviceId,
String market, long strategyId,
Long barTimeSec) {
return evaluate(userId, deviceId, market, strategyId, barTimeSec, null);
}
@Transactional(readOnly = true)
public LiveConditionStatusDto evaluate(Long userId, String deviceId,
String market, long strategyId,
Long barTimeSec,
Map<String, Map<String, Object>> indicatorParamsOverride) {
Optional<GcStrategy> opt = strategyRepo.findById(strategyId);
if (opt.isEmpty()) {
return empty(market, strategyId);
@@ -60,7 +75,9 @@ public class LiveConditionStatusService {
GcStrategy strategy = opt.get();
Map<String, Map<String, Object>> params =
indicatorSettingsService.getAll(userId, deviceId);
mergeIndicatorParams(
indicatorSettingsService.getAll(userId, deviceId),
indicatorParamsOverride);
Map<String, Map<String, Object>> visual =
indicatorSettingsService.getAllVisual(userId, deviceId);
@@ -72,18 +89,20 @@ public class LiveConditionStatusService {
return empty(market, strategyId);
}
// 마켓 구독 + 비동기 warm-up 시작 — 현재 봉 유무에 따라 즉시 or 수집 후 평가
// 마켓 구독 + warm-up — 과거 봉 평가 시 더 많은 이력 확보
int warmupBars = barTimeSec != null ? 500 : 60;
Set<String> timeframes = new LinkedHashSet<>();
for (PendingCond p : pending) {
String tf = LiveStrategyTimeframeService.normalize(p.timeframe());
timeframes.add(tf);
subscriptionManager.ensureBarsReadySync(market, tf, 60);
subscriptionManager.ensureBarsReadySync(market, tf, warmupBars);
}
subscriptionManager.ensureBarsReadySync(market, "1m", 60);
subscriptionManager.ensureBarsReadySync(market, "1m", warmupBars);
List<LiveConditionRowDto> rows = new ArrayList<>();
int met = 0;
int evaluable = 0;
Integer primaryEvalIndex = null;
for (PendingCond p : pending) {
String tf = LiveStrategyTimeframeService.normalize(p.timeframe());
@@ -96,7 +115,8 @@ public class LiveConditionStatusService {
rows.add(toRowUnevaluated(p, visual));
continue;
}
int index = series.getEndIndex();
int index = resolveBarIndex(series, barTimeSec);
if (primaryEvalIndex == null) primaryEvalIndex = index;
try {
ObjectNode wrapper = objectMapper.createObjectNode();
wrapper.put("type", "CONDITION");
@@ -128,9 +148,9 @@ public class LiveConditionStatusService {
// [항목6] 전체 논리 트리 평가 — matchRate 와 달리 AND/OR/NOT 게이트 완전 반영
Boolean overallEntryMet = evaluateOverallRule(
strategy.getBuyConditionJson(), market, params, visual);
strategy.getBuyConditionJson(), market, params, visual, barTimeSec);
Boolean overallExitMet = evaluateOverallRule(
strategy.getSellConditionJson(), market, params, visual);
strategy.getSellConditionJson(), market, params, visual, barTimeSec);
return LiveConditionStatusDto.builder()
.market(market)
@@ -141,9 +161,36 @@ public class LiveConditionStatusService {
.overallExitMet(overallExitMet)
.rows(rows)
.updatedAt(System.currentTimeMillis())
.barTimeSec(barTimeSec)
.evalBarIndex(primaryEvalIndex)
.build();
}
/**
* barTimeSec(초)에 가장 가까운 bar index — null 이면 최신 봉.
* 차트·업비트 API 와 동일하게 봉 시작(open) 시각 기준으로 매칭한다.
*/
private int resolveBarIndex(BarSeries series, Long barTimeSec) {
if (barTimeSec == null) return series.getEndIndex();
int bestIdx = series.getEndIndex();
long bestDist = Long.MAX_VALUE;
for (int i = series.getBeginIndex(); i <= series.getEndIndex(); i++) {
long openSec = barOpenEpochSec(series, i);
long dist = Math.abs(openSec - barTimeSec);
if (dist < bestDist) {
bestDist = dist;
bestIdx = i;
}
}
return bestIdx;
}
/** Ta4j Bar → 차트용 봉 시작 시각(초) — BacktestingService.barStartEpoch 와 동일 */
private static long barOpenEpochSec(BarSeries series, int index) {
var bar = series.getBar(index);
return bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
}
private LiveConditionStatusDto empty(String market, long strategyId) {
return LiveConditionStatusDto.builder()
.market(market)
@@ -169,7 +216,8 @@ public class LiveConditionStatusService {
*/
private Boolean evaluateOverallRule(String conditionJson, String market,
Map<String, Map<String, Object>> params,
Map<String, Map<String, Object>> visual) {
Map<String, Map<String, Object>> visual,
Long barTimeSec) {
if (conditionJson == null || conditionJson.isBlank()) return null;
try {
com.fasterxml.jackson.databind.JsonNode dsl =
@@ -191,7 +239,7 @@ public class LiveConditionStatusService {
new StrategyDslToTa4jAdapter.RuleBuildContext(
series, params, visual, market, ta4jStorage, false, java.util.Map.of());
org.ta4j.core.Rule rule = adapter.toRule(dsl, ctx);
return rule.isSatisfied(series.getEndIndex(), null);
return rule.isSatisfied(resolveBarIndex(series, barTimeSec), null);
} catch (Exception e) {
log.debug("[LiveCondition] overall rule eval fail market={}: {}", market, e.getMessage());
return null;
@@ -421,4 +469,24 @@ public class LiveConditionStatusService {
return indicatorType;
}
private static Map<String, Map<String, Object>> mergeIndicatorParams(
Map<String, Map<String, Object>> base,
Map<String, Map<String, Object>> override) {
if (override == null || override.isEmpty()) return base;
if (base == null || base.isEmpty()) return override;
Map<String, Map<String, Object>> merged = new LinkedHashMap<>(base);
override.forEach((type, overrideParams) -> {
Map<String, Object> baseParams = merged.get(type);
if (baseParams == null || baseParams.isEmpty()) {
merged.put(type, overrideParams);
} else {
Map<String, Object> inner = new LinkedHashMap<>(baseParams);
inner.putAll(overrideParams);
merged.put(type, inner);
}
});
return merged;
}
}