전략평가 화면 수정

This commit is contained in:
Macbook
2026-06-12 15:32:00 +09:00
parent 8aae8489ae
commit b560b9de5c
10 changed files with 413 additions and 13 deletions
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.goldenchart.dto.LiveConditionRowDto;
import com.goldenchart.dto.LiveConditionStatusDto;
import com.goldenchart.dto.OhlcvBar;
import com.goldenchart.entity.GcStrategy;
import com.goldenchart.repository.GcStrategyRepository;
import com.goldenchart.storage.Ta4jStorage;
@@ -68,6 +69,16 @@ public class LiveConditionStatusService {
String market, long strategyId,
Long barTimeSec,
Map<String, Map<String, Object>> indicatorParamsOverride) {
return evaluate(userId, deviceId, market, strategyId, barTimeSec, indicatorParamsOverride, null, null);
}
@Transactional(readOnly = true)
public LiveConditionStatusDto evaluate(Long userId, String deviceId,
String market, long strategyId,
Long barTimeSec,
Map<String, Map<String, Object>> indicatorParamsOverride,
List<OhlcvBar> chartBars,
String chartTimeframe) {
Optional<GcStrategy> opt = strategyRepo.findById(strategyId);
if (opt.isEmpty()) {
return empty(market, strategyId);
@@ -89,6 +100,11 @@ public class LiveConditionStatusService {
return empty(market, strategyId);
}
if (chartBars != null && !chartBars.isEmpty() && chartTimeframe != null && !chartTimeframe.isBlank()) {
return evaluateOnChartBars(strategy, market, strategyId, pending, chartBars, chartTimeframe,
barTimeSec, params, visual);
}
// 마켓 구독 + warm-up — 과거 봉 평가 시 더 많은 이력 확보
int warmupBars = barTimeSec != null ? 500 : 60;
Set<String> timeframes = new LinkedHashSet<>();
@@ -191,6 +207,137 @@ public class LiveConditionStatusService {
return bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
}
/**
* 차트·백테스트와 동일 OHLCV 로 조건 평가 — Ta4jStorage 와의 불일치 방지.
*/
private LiveConditionStatusDto evaluateOnChartBars(
GcStrategy strategy,
String market,
long strategyId,
List<PendingCond> pending,
List<OhlcvBar> chartBars,
String chartTimeframe,
Long barTimeSec,
Map<String, Map<String, Object>> params,
Map<String, Map<String, Object>> visual) {
try {
JsonNode buyDsl = objectMapper.readTree(
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null");
JsonNode sellDsl = objectMapper.readTree(
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null");
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(chartBars, primaryTf);
if (primarySeries.isEmpty()) {
return empty(market, strategyId);
}
Map<String, BarSeries> seriesOverrides = OhlcvBarSeriesSupport.buildSeriesOverrides(
primarySeries, primaryTf, buyDsl, sellDsl);
List<LiveConditionRowDto> rows = new ArrayList<>();
int met = 0;
int evaluable = 0;
Integer primaryEvalIndex = OhlcvBarSeriesSupport.resolveBarIndex(primarySeries, barTimeSec);
for (PendingCond p : pending) {
String tf = LiveStrategyTimeframeService.normalize(p.timeframe());
BarSeries series = resolveChartSeries(tf, primaryTf, primarySeries, seriesOverrides, market);
if (series == null || series.isEmpty()) {
rows.add(toRowUnevaluated(p, visual));
continue;
}
int index = OhlcvBarSeriesSupport.resolveBarIndex(series, barTimeSec);
try {
ObjectNode wrapper = objectMapper.createObjectNode();
wrapper.put("type", "CONDITION");
wrapper.set("condition", p.condition());
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
new StrategyDslToTa4jAdapter.RuleBuildContext(
series, params, visual, market, ta4jStorage, false, seriesOverrides);
Rule rule = adapter.toRule(wrapper, ctx);
boolean satisfied = rule.isSatisfied(index, null);
Double current = adapter.readConditionFieldValue(
p.condition(), series, params, index, true);
Double target = readTargetNumeric(p.condition(), visual);
if (target == null) {
target = adapter.readConditionFieldValue(
p.condition(), series, params, index, false);
}
rows.add(toRow(p, current, target, satisfied, series, params, index));
evaluable++;
if (satisfied) met++;
} catch (Exception e) {
log.debug("[LiveCondition:chart] eval fail {} {}: {}", market, p.id(), e.getMessage());
rows.add(toRowUnevaluated(p, visual));
}
}
int matchRate = evaluable > 0 ? Math.round(100f * met / evaluable) : 0;
Boolean overallEntryMet = evaluateOverallRuleOnChart(
buyDsl, primarySeries, seriesOverrides, market, params, visual, barTimeSec);
Boolean overallExitMet = evaluateOverallRuleOnChart(
sellDsl, primarySeries, seriesOverrides, market, params, visual, barTimeSec);
return LiveConditionStatusDto.builder()
.market(market)
.strategyId(strategyId)
.timeframe(primaryTf)
.matchRate(matchRate)
.overallEntryMet(overallEntryMet)
.overallExitMet(overallExitMet)
.rows(rows)
.updatedAt(System.currentTimeMillis())
.barTimeSec(barTimeSec)
.evalBarIndex(primaryEvalIndex)
.build();
} catch (Exception e) {
log.warn("[LiveCondition:chart] evaluate fail market={}: {}", market, e.getMessage());
return empty(market, strategyId);
}
}
private BarSeries resolveChartSeries(String tf, String primaryTf, BarSeries primarySeries,
Map<String, BarSeries> overrides, String market) {
String normalized = OhlcvBarSeriesSupport.normalizeTf(tf);
if (overrides.containsKey(normalized)) {
return overrides.get(normalized);
}
if (normalized.equals(primaryTf)) {
return primarySeries;
}
if (ta4jStorage.exists(market, normalized)) {
return ta4jStorage.getOrCreate(market, normalized);
}
return primarySeries;
}
private Boolean evaluateOverallRuleOnChart(JsonNode dsl, BarSeries primarySeries,
Map<String, BarSeries> seriesOverrides,
String market,
Map<String, Map<String, Object>> params,
Map<String, Map<String, Object>> visual,
Long barTimeSec) {
if (dsl == null || dsl.isNull()) return null;
try {
String primaryTf = extractPrimaryTimeframe(dsl);
BarSeries series = resolveChartSeries(primaryTf, OhlcvBarSeriesSupport.normalizeTf(primaryTf),
primarySeries, seriesOverrides, market);
if (series == null || series.isEmpty()) return null;
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
new StrategyDslToTa4jAdapter.RuleBuildContext(
series, params, visual, market, ta4jStorage, false, seriesOverrides);
Rule rule = adapter.toRule(dsl, ctx);
int index = OhlcvBarSeriesSupport.resolveBarIndex(series, barTimeSec);
return rule.isSatisfied(index, null);
} catch (Exception e) {
log.debug("[LiveCondition:chart] overall rule fail market={}: {}", market, e.getMessage());
return null;
}
}
private LiveConditionStatusDto empty(String market, long strategyId) {
return LiveConditionStatusDto.builder()
.market(market)