llm 동작 수정
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* 전략평가 AI 검증 — LLM·Ta4j 재평가를 HTTP 요청 스레드와 분리.
|
||||
*/
|
||||
@Configuration
|
||||
public class AiVerifyAsyncConfig {
|
||||
|
||||
@Bean(name = "aiVerifyExecutor")
|
||||
public Executor aiVerifyExecutor(
|
||||
@Value("${goldenchart.llm.ai-verify.pool-size:2}") int poolSize,
|
||||
@Value("${goldenchart.llm.ai-verify.queue-capacity:8}") int queueCapacity) {
|
||||
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
|
||||
ex.setThreadNamePrefix("ai-verify-");
|
||||
ex.setCorePoolSize(Math.max(1, poolSize));
|
||||
ex.setMaxPoolSize(Math.max(1, poolSize));
|
||||
ex.setQueueCapacity(queueCapacity);
|
||||
ex.setWaitForTasksToCompleteOnShutdown(true);
|
||||
ex.setAwaitTerminationSeconds(120);
|
||||
ex.initialize();
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.LlmTestResponse;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStartResponse;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStatusResponse;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyRequest;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyResponse;
|
||||
import com.goldenchart.service.StrategyEvaluationAiVerifyJobService;
|
||||
import com.goldenchart.service.StrategyEvaluationLlmService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -13,7 +16,9 @@ import java.util.Map;
|
||||
/**
|
||||
* 전략 평가 부가 기능.
|
||||
*
|
||||
* POST /api/strategy/evaluation/ai-verify — LLM 기반 평가·시그널 일치 검증
|
||||
* POST /api/strategy/evaluation/ai-verify/jobs — AI 검증 비동기 시작 (jobId 즉시 반환)
|
||||
* GET /api/strategy/evaluation/ai-verify/jobs/{jobId} — job 상태·결과 조회
|
||||
* POST /api/strategy/evaluation/ai-verify — (레거시) 동기 검증
|
||||
* POST /api/strategy/evaluation/llm-test — LLM 연결 테스트
|
||||
*/
|
||||
@RestController
|
||||
@@ -22,6 +27,45 @@ import java.util.Map;
|
||||
public class StrategyEvaluationController {
|
||||
|
||||
private final StrategyEvaluationLlmService llmService;
|
||||
private final StrategyEvaluationAiVerifyJobService aiVerifyJobService;
|
||||
|
||||
@PostMapping("/ai-verify/jobs")
|
||||
public ResponseEntity<?> startAiVerifyJob(
|
||||
@RequestBody StrategyEvaluationAiVerifyRequest body,
|
||||
@RequestHeader Map<String, String> headers,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
TradingControllerSupport.requireRegisteredUser(headers);
|
||||
if (body == null || body.getContext() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "context 가 필요합니다."));
|
||||
}
|
||||
try {
|
||||
Long userId = TradingControllerSupport.parseUserId(headers);
|
||||
StrategyEvaluationAiVerifyJobStartResponse res =
|
||||
aiVerifyJobService.startJob(body.getContext(), userId, deviceId);
|
||||
return ResponseEntity.accepted().body(res);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
} catch (IllegalStateException e) {
|
||||
return ResponseEntity.status(503).body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/ai-verify/jobs/{jobId}")
|
||||
public ResponseEntity<?> getAiVerifyJob(
|
||||
@PathVariable String jobId,
|
||||
@RequestHeader Map<String, String> headers) {
|
||||
TradingControllerSupport.requireRegisteredUser(headers);
|
||||
try {
|
||||
Long userId = TradingControllerSupport.parseUserId(headers);
|
||||
StrategyEvaluationAiVerifyJobStatusResponse res = aiVerifyJobService.getJob(jobId, userId);
|
||||
if (res == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(res);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/ai-verify")
|
||||
public ResponseEntity<?> aiVerify(
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StrategyEvaluationAiVerifyJobStartResponse {
|
||||
private String jobId;
|
||||
/** QUEUED | RUNNING */
|
||||
private String status;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StrategyEvaluationAiVerifyJobStatusResponse {
|
||||
private String jobId;
|
||||
/** QUEUED | RUNNING | COMPLETED | FAILED */
|
||||
private String status;
|
||||
private String error;
|
||||
private StrategyEvaluationAiVerifyResponse result;
|
||||
private long createdAtMs;
|
||||
private Long completedAtMs;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class StrategyEvaluationAiVerifyJobRunner {
|
||||
|
||||
private final StrategyEvaluationLlmService llmService;
|
||||
private final StrategyEvaluationAiVerifyJobService jobService;
|
||||
|
||||
@Async("aiVerifyExecutor")
|
||||
public void execute(String jobId, JsonNode context, Long userId, String deviceId) {
|
||||
jobService.markRunning(jobId);
|
||||
try {
|
||||
var result = llmService.verify(context, userId, deviceId);
|
||||
jobService.markCompleted(jobId, result);
|
||||
} catch (Exception e) {
|
||||
log.warn("[AiVerifyJob] fail jobId={}: {}", jobId, e.getMessage());
|
||||
String msg = e.getMessage() != null && !e.getMessage().isBlank()
|
||||
? e.getMessage()
|
||||
: "AI 검증 중 오류가 발생했습니다.";
|
||||
jobService.markFailed(jobId, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStartResponse;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStatusResponse;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StrategyEvaluationAiVerifyJobService {
|
||||
|
||||
private static final long JOB_TTL_MS = 60L * 60 * 1000;
|
||||
|
||||
private final StrategyEvaluationAiVerifyJobRunner jobRunner;
|
||||
|
||||
private final ConcurrentHashMap<String, JobRecord> jobs = new ConcurrentHashMap<>();
|
||||
|
||||
public StrategyEvaluationAiVerifyJobStartResponse startJob(JsonNode context, Long userId, String deviceId) {
|
||||
purgeExpired();
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
JobRecord record = new JobRecord(jobId, userId, System.currentTimeMillis());
|
||||
jobs.put(jobId, record);
|
||||
jobRunner.execute(jobId, context, userId, deviceId);
|
||||
return StrategyEvaluationAiVerifyJobStartResponse.builder()
|
||||
.jobId(jobId)
|
||||
.status(record.status)
|
||||
.build();
|
||||
}
|
||||
|
||||
public StrategyEvaluationAiVerifyJobStatusResponse getJob(String jobId, Long userId) {
|
||||
purgeExpired();
|
||||
JobRecord record = jobs.get(jobId);
|
||||
if (record == null || !record.userId.equals(userId)) {
|
||||
return null;
|
||||
}
|
||||
return toResponse(record);
|
||||
}
|
||||
|
||||
void markRunning(String jobId) {
|
||||
JobRecord record = jobs.get(jobId);
|
||||
if (record != null) {
|
||||
record.status = "RUNNING";
|
||||
}
|
||||
}
|
||||
|
||||
void markCompleted(String jobId, StrategyEvaluationAiVerifyResponse result) {
|
||||
JobRecord record = jobs.get(jobId);
|
||||
if (record != null) {
|
||||
record.status = "COMPLETED";
|
||||
record.result = result;
|
||||
record.error = null;
|
||||
record.completedAtMs = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
void markFailed(String jobId, String error) {
|
||||
JobRecord record = jobs.get(jobId);
|
||||
if (record != null) {
|
||||
record.status = "FAILED";
|
||||
record.error = error;
|
||||
record.completedAtMs = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private static StrategyEvaluationAiVerifyJobStatusResponse toResponse(JobRecord record) {
|
||||
return StrategyEvaluationAiVerifyJobStatusResponse.builder()
|
||||
.jobId(record.jobId)
|
||||
.status(record.status)
|
||||
.error(record.error)
|
||||
.result(record.result)
|
||||
.createdAtMs(record.createdAtMs)
|
||||
.completedAtMs(record.completedAtMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void purgeExpired() {
|
||||
long cutoff = System.currentTimeMillis() - JOB_TTL_MS;
|
||||
Iterator<Map.Entry<String, JobRecord>> it = jobs.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, JobRecord> e = it.next();
|
||||
long ts = e.getValue().completedAtMs != null
|
||||
? e.getValue().completedAtMs
|
||||
: e.getValue().createdAtMs;
|
||||
if (ts < cutoff) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class JobRecord {
|
||||
private final String jobId;
|
||||
private final Long userId;
|
||||
private final long createdAtMs;
|
||||
private String status = "QUEUED";
|
||||
private String error;
|
||||
private StrategyEvaluationAiVerifyResponse result;
|
||||
private Long completedAtMs;
|
||||
|
||||
private JobRecord(String jobId, Long userId, long createdAtMs) {
|
||||
this.jobId = jobId;
|
||||
this.userId = userId;
|
||||
this.createdAtMs = createdAtMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,31 +26,36 @@ public class StrategyEvaluationLlmService {
|
||||
|
||||
private static final String SYSTEM_PROMPT = """
|
||||
당신은 GoldenChart 전략 평가(Ta4j Rule) 검증 전문가입니다.
|
||||
사용자 JSON에는 선택 봉 시점의 조건 평가 결과, 차트 매매 시그널, 전략 DSL 요약이 포함됩니다.
|
||||
ta4jDiagnostics 섹션은 서버가 동일 경로로 Ta4j Rule 을 재빌드·재평가한 결과(Rule descriptor, trace 로그, frontend vs backend 비교)입니다.
|
||||
사용자 JSON에는 선택 봉 시점의 조건 평가, 차트 시그널, 전략 DSL, llmGuidance가 포함됩니다.
|
||||
ta4jDiagnostics는 서버 Ta4j 재평가 결과(frontend vs backend 비교)입니다.
|
||||
|
||||
【중요 — 오판 방지】
|
||||
- evaluation.buyConditions/sellConditions 각 항목의 dslCondition·dslExpression·evaluationSemantics를
|
||||
판단의 1차 근거로 사용하세요. UI용 thresholdLabel(예: "> 20")은 컨텍스트에 없습니다.
|
||||
- CROSS_UP/CROSS_DOWN은 GT/LT(수준 비교)와 다릅니다. 교차는 직전봉·현재봉 2봉 이벤트입니다.
|
||||
현재값>임계값인데 CROSS_UP 미충족은 정상일 수 있습니다(이미 돌파 후 유지).
|
||||
- matchRate는 leaf 조건 satisfied=true 비율입니다. 0%%는 leaf 전부 미충족이지,
|
||||
Rule↔시그널 불일치를 뜻하지 않습니다.
|
||||
- deterministicHints.signalMatchesOverallRule이 "대체로 일치"이면 overall Rule과 차트 시그널은 일치합니다.
|
||||
|
||||
다음을 한국어로 분석하세요:
|
||||
1) 선택 봉에서 매수/매도 전체 Rule(overallEntryMet/overallExitMet)과 개별 조건(satisfied)이 논리적으로 일치하는지
|
||||
2) ta4jDiagnostics.reEvaluation·entryRule/exitRule.satisfiedAtEvalIndex 와 프론트 evaluation 이 일치하는지
|
||||
3) entryRule/exitRule.evaluationTraceLog 로 Rule 평가 경로를 추적해 불일치 원인을 설명
|
||||
4) 차트에 표시된 매매 시그널(selectedBarSignals, barSignalHighlight)이 평가 결과와 맞는지
|
||||
5) 시그널이 없을 때: 실제로 조건 미충족이라 시그널이 없는 것인지, 로직·데이터 불일치 가능성이 있는지
|
||||
6) 의심 지점이 있으면 구체적 조건 ID·지표·수치·Rule 타입을 인용
|
||||
1) dslExpression·evaluationSemantics 기준으로 satisfied·overallEntryMet/overallExitMet 논리 일치
|
||||
2) ta4jDiagnostics.reEvaluation·entryRule/exitRule.satisfiedAtEvalIndex 와 evaluation 일치
|
||||
3) 차트 시그널(selectedBarSignals, barSignalHighlight)과 overall Rule 일치
|
||||
4) 실제 불일치만 "오류 의심" — CROSS 조건의 정상 미충족은 "정상"
|
||||
|
||||
출력 형식(마크다운):
|
||||
## 종합 판정
|
||||
(정상 / 주의 / 오류 의심 — 한 줄 요약)
|
||||
|
||||
## 조건 평가 검증
|
||||
(매수·매도 각각 bullet)
|
||||
(매수·매도 각각 bullet — dslExpression 인용)
|
||||
|
||||
## 차트 시그널 검증
|
||||
(선택 봉 시그널 vs Rule 결과)
|
||||
|
||||
## 결론 및 권장 조치
|
||||
(1~3문장)
|
||||
|
||||
로직 문제(종합 판정이 주의·오류 의심)인 경우, 위 분석에서 불일치 원인·조건 ID·지표를 구체적으로 명시하세요.
|
||||
""";
|
||||
|
||||
private static final String TEST_USER_PROMPT = "ping — 연결 테스트입니다. 한 문장으로 응답해 주세요.";
|
||||
|
||||
Reference in New Issue
Block a user