전략평가 메뉴 추가
This commit is contained in:
@@ -7,7 +7,7 @@ public final class MenuIds {
|
||||
private MenuIds() {}
|
||||
|
||||
public static final List<String> ALL = List.of(
|
||||
"dashboard", "chart", "paper", "virtual", "trend-search", "verification-board", "strategy", "strategy-editor", "backtest", "analysis-report", "notifications", "settings",
|
||||
"dashboard", "chart", "paper", "virtual", "trend-search", "verification-board", "strategy", "strategy-editor", "strategy-evaluation", "backtest", "analysis-report", "notifications", "settings",
|
||||
"settings_general", "settings_chart", "settings_indicators", "settings_backtest",
|
||||
"settings_strategy", "settings_trading", "settings_paper", "settings_virtual", "settings_live",
|
||||
"settings_trend-search",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.LiveConditionEvaluateRequest;
|
||||
import com.goldenchart.dto.LiveConditionStatusDto;
|
||||
import com.goldenchart.service.LiveConditionStatusService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -12,6 +13,7 @@ import java.util.Map;
|
||||
* 가상투자 — 실시간 조건 충족 현황 API.
|
||||
*
|
||||
* GET /api/strategy/live-conditions?market=KRW-BTC&strategyId=1
|
||||
* POST /api/strategy/live-conditions/evaluate — 지표 파라미터 오버라이드 (전략 평가)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/strategy/live-conditions")
|
||||
@@ -24,9 +26,28 @@ public class LiveConditionController {
|
||||
public ResponseEntity<LiveConditionStatusDto> get(
|
||||
@RequestParam String market,
|
||||
@RequestParam long strategyId,
|
||||
@RequestParam(required = false) Long barTimeSec,
|
||||
@RequestHeader Map<String, String> headers) {
|
||||
long uid = TradingControllerSupport.requireRegisteredUser(headers);
|
||||
String deviceId = headers.get("x-device-id");
|
||||
return ResponseEntity.ok(service.evaluate(uid, deviceId, market, strategyId));
|
||||
return ResponseEntity.ok(service.evaluate(uid, deviceId, market, strategyId, barTimeSec));
|
||||
}
|
||||
|
||||
@PostMapping("/evaluate")
|
||||
public ResponseEntity<LiveConditionStatusDto> evaluateAtBar(
|
||||
@RequestBody LiveConditionEvaluateRequest body,
|
||||
@RequestHeader Map<String, String> headers) {
|
||||
long uid = TradingControllerSupport.requireRegisteredUser(headers);
|
||||
String deviceId = headers.get("x-device-id");
|
||||
if (body == null || body.getMarket() == null || body.getMarket().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
return ResponseEntity.ok(service.evaluate(
|
||||
uid,
|
||||
deviceId,
|
||||
body.getMarket(),
|
||||
body.getStrategyId(),
|
||||
body.getBarTimeSec(),
|
||||
body.getIndicatorParams()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/** 전략 평가 — 특정 봉 + 지표 파라미터 오버라이드 조건 평가 */
|
||||
@Data
|
||||
public class LiveConditionEvaluateRequest {
|
||||
private String market;
|
||||
private long strategyId;
|
||||
private Long barTimeSec;
|
||||
/** indicatorType → param map (DB 저장값 위에 덮어씀) */
|
||||
private Map<String, Map<String, Object>> indicatorParams;
|
||||
}
|
||||
@@ -41,4 +41,9 @@ public class LiveConditionStatusDto {
|
||||
|
||||
private List<LiveConditionRowDto> rows;
|
||||
private long updatedAt;
|
||||
|
||||
/** 평가 기준 봉 시각(초) — null 이면 최신 봉 */
|
||||
private Long barTimeSec;
|
||||
/** barTimeSec 에 대응하는 시리즈 내 bar index */
|
||||
private Integer evalBarIndex;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user