diff --git a/backend/src/main/java/com/goldenchart/service/StrategyAgentChatService.java b/backend/src/main/java/com/goldenchart/service/StrategyAgentChatService.java
index 750670c..5d134b6 100644
--- a/backend/src/main/java/com/goldenchart/service/StrategyAgentChatService.java
+++ b/backend/src/main/java/com/goldenchart/service/StrategyAgentChatService.java
@@ -24,8 +24,8 @@ import java.util.regex.Pattern;
/**
* AI 전략 에이전트 — 자연어 멀티턴 대화로 전략 DSL(LogicNode 트리)을 생성/수정한다.
*
- *
설정에 저장된 LLM(OpenAI 호환)을 사용하며, 정보가 부족하면 되묻고(action=ask),
- * 충분하면 전략을 생성/수정한다(action=build).
+ *
설정에 저장된 LLM(OpenAI 호환)을 사용한다. 주식/코인 매매 전략 전문가로서 조언·전략 추천을 제공하며,
+ * 정보가 부족하면 되묻고(action=ask), 충분하면 전략을 생성/수정하고(action=build), 초기화 요청 시 비운다(action=clear).
*/
@Service
@RequiredArgsConstructor
@@ -37,16 +37,49 @@ public class StrategyAgentChatService {
Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```", Pattern.CASE_INSENSITIVE);
private static final String SYSTEM_PROMPT = """
- 당신은 GoldenChart 전략편집기의 AI 전략 에이전트입니다.
- 사용자의 자연어 요청을 받아 매매 시그널 전략을 GoldenChart 전략 DSL(JSON)로 구성/수정합니다.
+ 당신은 GoldenChart 전략편집기의 AI 전략 에이전트이자, 보조지표·캔들차트·차트패턴을 활용한
+ 주식/코인 매매 전략에 정통한 전문 트레이더입니다.
+ 사용자의 자연어 요청을 받아 매매 시그널 전략을 GoldenChart 전략 DSL(JSON)로 구성/수정하며,
+ 동시에 전문가 관점의 조언을 제공합니다.
+
+ 【전문가 역할 — 조언】
+ - 단순히 시키는 대로만 만들지 말고, 트레이더의 시각에서 더 나은 구성을 제안하세요.
+ 예: 추세장/횡보장 적합성, 휩쏘(whipsaw) 위험, 후행성·과최적화 위험, 손절·필터 보강, 다중 시간봉 정합성.
+ - 지표의 의미와 신호의 강·약을 짧게 설명하고(왜 이 조건이 매수/매도 신호인지), 보완 필터를 권할 수 있으면 권하세요.
+ - 비현실적이거나 모순된 조건(예: 같은 봉에서 과매수 매수 + 과매도 매도)은 정중히 지적하고 대안을 제시하세요.
+ - 설명은 간결한 한국어로. 과도하게 길게 쓰지 말고 핵심 위주로.
+
+ 【전략 추천 — recommend】
+ - 사용자가 "추천해줘"(예: "일목균형표 매수/매도 전략 추천해줘")라고 하면, 보통 2~4개의 구체적 전략 후보를
+ 번호로 제시하고 각 후보의 신호 논리·장단점·적합 시장을 한두 줄로 설명한 뒤,
+ "몇 번을 적용할까요?"처럼 선택을 유도하세요. 이때는 DSL을 만들지 말고 질문 턴으로 둡니다. (action=ask)
+ - 사용자가 번호를 고르거나 "그걸로 해줘"라고 하면 해당 후보를 DSL로 구성합니다. (action=build)
+ - 사용자가 처음부터 "바로 만들어줘"라고 하면 가장 대표적인 후보 하나를 바로 build 해도 됩니다.
+
+ 【평가 결과 해석·문제 진단 — analyze】
+ - 사용자가 평가탭 결과 해석/분석/문제 진단(예: "매수 시그널이 왜 안 나와?", "이 결과 해석해줘",
+ "매도가 너무 자주 나오는 원인 분석해줘")을 요청하면, 【현재 편집기 상태】의 evaluation 데이터
+ (market·timeframe·barCount·signalsTotal·buyCount·sellCount·recentSignals·summary)와 현재 DSL을 근거로
+ 전문 트레이더 관점에서 분석합니다. 이때는 보통 설명만 제공하고 DSL은 바꾸지 않습니다. (action=ask)
+ - evaluation 데이터가 없거나 signalsTotal이 비어있으면, 먼저 "평가탭에서 평가를 실행해 주세요"라고 안내하세요.
+ - 시그널이 전혀/거의 안 나오는 흔한 원인과 해결책을 구체적으로 제시:
+ · 조건 과다 AND 결합(특히 여러 CROSS 이벤트의 동시 충족은 매우 드묾) → 핵심 조건만 남기거나 일부를 EXISTS_IN(N봉 이내)로 완화
+ · 임계값이 극단적(예: RSI 10/90) → 30/70 등 현실값으로 조정
+ · CROSS_UP/DOWN을 같은 봉에서 다수 요구 → 윈도우(candleRangeMode=EXISTS_IN, candleRange=N)로 시간차 허용
+ · 타임프레임 불일치 / 평가봉 수 부족(워밍업 미달로 일목·장기 MA 미계산) → 평가 구간 확대
+ · 상호배타 조건(같은 봉 과매수 매수 + 과매도) → 논리 재구성
+ - 시그널이 과도하게 많으면: 필터(추세 방향, 거래량 확인) 추가, AND 보강, 윈도우 축소를 권하세요.
+ - 분석 끝에 개선책을 제안하고 "적용해 드릴까요?"로 물으세요. 사용자가 동의하면 해당 수정안을 build로 반영합니다.
【행동 규칙 — 멀티턴 에이전트】
- 요청이 모호하거나 핵심 정보가 빠졌으면, 전략을 만들지 말고 한국어로 짧고 구체적으로 되물으세요. (action=ask)
핵심 정보 예: 매수/매도 구분, 지표 기간(예: 일목 9/26/52), 임계값(예: RSI 30), 캔들 종류(예: 5분봉),
"N봉 이내" 같은 윈도우 조건 여부.
- 정보가 충분하면 전략을 생성하거나, 이미 전략이 있으면 그것을 수정합니다. (action=build)
+ - 사용자가 "초기화", "리셋", "새 전략", "새 조건", "처음부터 다시", "다 지워" 등 전략을 비우고 처음부터
+ 새로 작성하길 원하면, 모든 조건을 비웁니다. (action=clear)
- 현재 편집기 상태(【현재 편집기 상태】의 buyCondition/sellCondition)가 비어있지 않으면, 처음부터 새로
- 만들지 말고 기존 DSL을 최소 변경으로 수정하세요.
+ 만들지 말고 기존 DSL을 최소 변경으로 수정하세요. (단, 초기화 요청이면 clear)
- 한 번에 한두 가지만 묻고, 합리적인 기본값이 있으면 가정하고 진행하되 가정한 내용을 설명에 명시하세요.
【출력 형식】
@@ -55,6 +88,7 @@ public class StrategyAgentChatService {
```json
{ "action": "build", "buyCondition": , "sellCondition": }
```
+ - action=clear일 때는 설명 뒤에 ```json { "action": "clear" } ``` 블록만 추가합니다(조건 없음).
- action=ask일 때는 json 코드블록을 절대 넣지 마세요(질문만).
- 매수 전략만 요청되면 sellCondition은 null로 둡니다(반대도 동일).
@@ -99,6 +133,44 @@ public class StrategyAgentChatService {
- "최근 N봉 내 한 번도 없음" → "NOT_EXISTS_IN"
- 미지정 시 현재 봉만 평가(CURRENT).
+ 【지표의 의미와 수학 — 조건 설계 기준】
+ - 추세 지표(MA/EMA/일목/볼린저 중심선): 방향성. 골든크로스(단기 MA가 장기 MA를 CROSS_UP)=매수 전환,
+ 데드크로스(CROSS_DOWN)=매도 전환. 정배열/역배열로 추세 강도 판단.
+ - 모멘텀/오실레이터(RSI·STOCH·CCI·Williams%R): 과매수/과매도 + 교차. RSI<30 과매도, >70 과매수.
+ "과매도에서 상향돌파"는 RSI가 30선을 CROSS_UP(반등 매수), "과매수 이탈"은 70선 CROSS_DOWN(매도).
+ 수준 비교(GT/LT)와 교차(CROSS_UP/DOWN)는 다릅니다 — 교차는 직전봉→현재봉 2봉 이벤트.
+ - MACD: MACD_LINE이 SIGNAL_LINE을 CROSS_UP=매수, 0선(HL_MID) 상향돌파=추세 강화. 히스토그램 부호 전환도 신호.
+ - 볼린저밴드: LOWER_BAND 터치 후 반등=평균회귀 매수, UPPER_BAND 돌파=추세 추종. 밴드폭으로 변동성 판단.
+ - 거래량: 신호의 확인용 필터로 권장. VOLUME_GT_MA_RATIO로 평균 대비 급증을 동반한 돌파만 채택하면 휩쏘 감소.
+ - 다중 조건 결합: 추세(방향) AND 모멘텀(타이밍) AND 거래량(확인) 형태가 견고. 단일 오실레이터만 쓰면 추세장에서 약함.
+ - 좋은 매수=여러 근거의 AND, 좋은 매도=빠른 이탈 위해 OR(손절·반전 중 하나라도)가 흔함.
+
+ 【일목균형표(ICHIMOKU) 전략 지식 — 추천 재료】
+ - 구성: 전환선(CONVERSION_LINE, 9), 기준선(BASE_LINE, 26), 선행스팬1/2(LEADING_SPAN1/2, 구름), 후행스팬(LAGGING_SPAN, 26봉 전).
+ - 대표 매수 신호:
+ 1) 전환선이 기준선을 상향돌파(CROSS_UP) — 단기 모멘텀 전환(약~중 신호).
+ 2) 가격이 구름 위로 상향돌파(CLOUD_BREAK_UP) 또는 구름 위(ABOVE_CLOUD) — 추세 전환/유지(강 신호).
+ 3) 후행스팬이 26봉 전 종가를 상향돌파(LAGGING_GT_PRICE) — 후행 확인(신뢰도↑, 후행성 있음).
+ 4) 삼역호전(전환>기준 AND 가격>구름 AND 후행>가격)=강력 매수. AND로 결합.
+ - 대표 매도 신호: 전환선<기준선(CROSS_DOWN), 가격 구름 하향돌파(CLOUD_BREAK_DOWN/BELOW_CLOUD), 후행<가격(LAGGING_LT_PRICE).
+ - 추천 시 강도/지연 트레이드오프를 설명: (1)은 빠르지만 휩쏘 위험, (3)/삼역호전은 느리지만 신뢰도 높음.
+
+ 【예시 0 — 전략 추천(ask)】
+ 사용자: "일목균형표 매수 전략 추천해줘"
+ 응답(요지): "일목균형표 매수 전략 3가지를 추천드립니다.
+ 1) 전환선 > 기준선 상향돌파 — 빠른 진입, 휩쏘 주의(횡보장 비권장)
+ 2) 가격의 구름 상향돌파 — 추세 전환 포착, 신뢰도 중상
+ 3) 삼역호전(전환>기준 AND 가격>구름 위 AND 후행스팬>가격) — 가장 보수적·신뢰도 높음, 진입 지연
+ 어떤 전략을 적용할까요? (예: 3번, 또는 '거래량 필터도 추가')" (json 블록 없음)
+
+ 【예시 0-A — 평가 결과 분석(ask)】
+ 사용자: "매수 시그널이 하나도 안 나와. 왜 그런지 분석해줘"
+ 응답(요지, evaluation 데이터 기반): "현재 매수 조건이 '전환선>기준 상향돌파 AND 후행스팬>가격 상향돌파 AND
+ RSI 80 상향돌파' 3개의 CROSS 이벤트를 같은 봉에서 모두 요구합니다. 서로 다른 교차가 한 봉에 동시에 일어날
+ 확률은 매우 낮아 시그널이 0건입니다. 또 RSI 80 돌파는 과매수 영역이라 매수 타이밍과 상충합니다.
+ 해결책: ① RSI 조건 제거 또는 30선 상향돌파로 변경, ② 후행스팬 조건을 '최근 4봉 이내(EXISTS_IN)'로 완화,
+ ③ 핵심은 '전환선>기준 상향돌파'만 남기기. 이 방향으로 수정해 적용해 드릴까요?" (json 블록 없음)
+
【예시 1 — 정보 부족(ask)】
사용자: "골든크로스로 매수해줘"
응답: "어떤 이동평균선 조합으로 골든크로스를 잡을까요? 예를 들어 단기 5 / 장기 20처럼 기간을 알려주세요. 캔들은 어떤 분봉인가요?"
diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx
index 820c1d9..3ef5aca 100644
--- a/frontend/src/components/StrategyEditorPage.tsx
+++ b/frontend/src/components/StrategyEditorPage.tsx
@@ -1464,17 +1464,6 @@ export default function StrategyEditorPage({
[templateRows, templateSearch],
);
- const buildAiContext = useCallback((): AiStrategyContext => {
- const buyEncoded = encodeConditionForSave(buyEditorState);
- const sellEncoded = encodeConditionForSave(sellEditorState);
- return {
- buyCondition: buyEncoded,
- sellCondition: sellEncoded,
- signalTab: buySellTab(signalTab),
- candleType: currentEditorState.startMeta[START_NODE_ID]?.candleType,
- };
- }, [buyEditorState, sellEditorState, signalTab, currentEditorState]);
-
const applyAiStrategy = useCallback((buyDsl: LogicNode | null, sellDsl: LogicNode | null) => {
if (editorMode === 'graph') layoutFlushRef.current?.();
@@ -1799,6 +1788,52 @@ export default function StrategyEditorPage({
evalCommissionRate,
]);
+ const buildAiContext = useCallback((): AiStrategyContext => {
+ const buyEncoded = encodeConditionForSave(buyEditorState);
+ const sellEncoded = encodeConditionForSave(sellEditorState);
+
+ const evaluation = evalBacktestSignals.length > 0 || signalTab === 'eval'
+ ? {
+ market: evalMarket,
+ timeframe: qbStrategyTimeframe,
+ barCount: evalBars.length,
+ evaluationBarCount: evalWindowMeta.evaluationBarCount,
+ scanRunning: evalSignalScanRunning,
+ signalsTotal: evalBacktestSignals.length,
+ buyCount: evalBacktestSignals.filter(s => s.type === 'BUY').length,
+ sellCount: evalBacktestSignals.filter(s => s.type === 'SELL').length,
+ recentSignals: evalBacktestSignals.slice(-12).map(s => ({
+ type: s.type,
+ time: s.time,
+ timeLabel: new Date(s.time * 1000).toLocaleString('ko-KR'),
+ price: s.price,
+ barIndex: s.barIndex,
+ })),
+ summary: evalAnalysisSummary ?? null,
+ }
+ : null;
+
+ return {
+ buyCondition: buyEncoded,
+ sellCondition: sellEncoded,
+ signalTab: buySellTab(signalTab),
+ candleType: currentEditorState.startMeta[START_NODE_ID]?.candleType,
+ evaluation,
+ };
+ }, [
+ buyEditorState,
+ sellEditorState,
+ signalTab,
+ currentEditorState,
+ evalBacktestSignals,
+ evalMarket,
+ qbStrategyTimeframe,
+ evalBars,
+ evalWindowMeta.evaluationBarCount,
+ evalSignalScanRunning,
+ evalAnalysisSummary,
+ ]);
+
const handleEvalSignalsChange = useCallback((signals: BacktestSignal[]) => {
setEvalBacktestSignals(signals);
}, []);
diff --git a/frontend/src/components/strategyEditor/AiStrategyPanel.tsx b/frontend/src/components/strategyEditor/AiStrategyPanel.tsx
index 3508ff5..b869468 100644
--- a/frontend/src/components/strategyEditor/AiStrategyPanel.tsx
+++ b/frontend/src/components/strategyEditor/AiStrategyPanel.tsx
@@ -10,6 +10,8 @@ export interface AiStrategyContext {
sellCondition: LogicNode | null;
signalTab: 'buy' | 'sell';
candleType?: string;
+ /** 평가탭 결과 (해석·문제 진단용) — 평가 미실행 시 null */
+ evaluation?: unknown;
[key: string]: unknown;
}
@@ -33,12 +35,24 @@ type Turn = {
};
const SUGGESTIONS = [
+ '일목균형표 매수 전략 추천해줘',
'전환선이 기준선을 상향돌파하면 매수',
- '26봉 전 후행스팬이 종가를 상향돌파하고 전환선이 기준선을 상향돌파하면 매수',
'RSI 30 이하에서 상향돌파하면 매수, 70 이상이면 매도',
- '지금 전략을 더 엄격하게 만들어줘',
+ '평가 결과를 해석하고 매수 시그널이 안 나오는 원인을 분석해줘',
];
+const CLEAR_REPLY =
+ '전략을 초기화했습니다. 모든 조건을 비워 처음 상태로 되돌렸어요. 원하는 매매 전략을 자연어로 설명해 주세요.';
+
+/** 초기화/리셋/새로 작성 의도 감지 — LLM 없이 즉시 처리 (서버 503에도 동작) */
+export function isResetRequest(text: string): boolean {
+ const t = text.trim();
+ if (!t) return false;
+ // "조건 추가"처럼 추가 의도가 섞이면 초기화로 보지 않음
+ if (/추가|더해|덧붙|넣어|보태/.test(t)) return false;
+ return /(초기화|리셋|reset|clear|처음\s*부터|처음\s*으로|다시\s*처음|새\s*전략|새로운\s*전략|새\s*조건|새로운\s*조건|전부\s*삭제|전체\s*삭제|모두\s*삭제|다\s*지워|다\s*삭제|비우[기게]|싹\s*비워|새로\s*시작|처음\s*상태)/i.test(t);
+}
+
const AiStrategyPanel: React.FC = ({ getContext, onApplyStrategy }) => {
const [turns, setTurns] = useState([]);
const [input, setInput] = useState('');
@@ -61,11 +75,37 @@ const AiStrategyPanel: React.FC = ({ getContext, onApplyStrategy }) => {
[turns],
);
+ /** 모든 조건을 비우고(초기화) 되돌리기 가능한 어시스턴트 턴 생성 */
+ const buildClearTurn = useCallback((ctx: AiStrategyContext, reply?: string): Turn => {
+ const snapshot = { buy: ctx.buyCondition, sell: ctx.sellCondition };
+ onApplyStrategy(null, null);
+ return {
+ id: nextId(),
+ role: 'assistant',
+ content: reply || CLEAR_REPLY,
+ action: 'build',
+ snapshot,
+ produced: { buy: null, sell: null },
+ };
+ }, [onApplyStrategy]);
+
const send = useCallback(async (text: string) => {
const trimmed = text.trim();
if (!trimmed || busy) return;
setError(null);
+ const ctx = getContext();
+
+ // 초기화/리셋 요청 — LLM 호출 없이 즉시 처리
+ if (isResetRequest(trimmed)) {
+ const userTurn: Turn = { id: nextId(), role: 'user', content: trimmed };
+ const clearTurn = buildClearTurn(ctx);
+ setInput('');
+ setTurns(prev => [...prev, userTurn, clearTurn]);
+ scrollToBottom();
+ return;
+ }
+
const userTurn: Turn = { id: nextId(), role: 'user', content: trimmed };
const outgoing: StrategyAgentChatMessage[] = [...history, { role: 'user', content: trimmed }];
setTurns(prev => [...prev, userTurn]);
@@ -73,11 +113,12 @@ const AiStrategyPanel: React.FC = ({ getContext, onApplyStrategy }) => {
setBusy(true);
scrollToBottom();
- const ctx = getContext();
try {
const res = await requestStrategyAgentChat(outgoing, ctx);
- if (res.action === 'build' && res.hasStrategy) {
+ if (res.action === 'clear') {
+ setTurns(prev => [...prev, buildClearTurn(ctx, res.reply)]);
+ } else if (res.action === 'build' && res.hasStrategy) {
const snapshot = { buy: ctx.buyCondition, sell: ctx.sellCondition };
// AI가 생략한 측은 기존 조건 유지
const produced = {
@@ -114,7 +155,7 @@ const AiStrategyPanel: React.FC = ({ getContext, onApplyStrategy }) => {
setBusy(false);
scrollToBottom();
}
- }, [busy, history, getContext, onApplyStrategy, scrollToBottom]);
+ }, [busy, history, getContext, onApplyStrategy, scrollToBottom, buildClearTurn]);
const handleRevert = useCallback((turnId: number) => {
setTurns(prev => prev.map(t => {
diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts
index a1374e9..b7a6406 100644
--- a/frontend/src/utils/backendApi.ts
+++ b/frontend/src/utils/backendApi.ts
@@ -1869,7 +1869,7 @@ export interface StrategyAgentChatMessage {
export interface StrategyAgentChatApiResponse {
reply: string;
- action: 'ask' | 'build';
+ action: 'ask' | 'build' | 'clear';
buyCondition?: unknown | null;
sellCondition?: unknown | null;
model?: string;
diff --git a/frontend/src/utils/strategyAgentChat.ts b/frontend/src/utils/strategyAgentChat.ts
index 26e0e60..7530b2e 100644
--- a/frontend/src/utils/strategyAgentChat.ts
+++ b/frontend/src/utils/strategyAgentChat.ts
@@ -12,7 +12,7 @@ export type { StrategyAgentChatMessage } from './backendApi';
export interface StrategyAgentResult {
reply: string;
- action: 'ask' | 'build';
+ action: 'ask' | 'build' | 'clear';
/** 검증·정규화된 매수 조건 (build이고 유효할 때만) */
buyCondition: LogicNode | null;
/** 검증·정규화된 매도 조건 */
@@ -93,7 +93,8 @@ export async function requestStrategyAgentChat(
context: unknown,
): Promise {
const res: StrategyAgentChatApiResponse = await postStrategyAgentChat(messages, context);
- const action = res.action === 'build' ? 'build' : 'ask';
+ const action: 'ask' | 'build' | 'clear' =
+ res.action === 'build' ? 'build' : res.action === 'clear' ? 'clear' : 'ask';
const buyCondition = action === 'build' ? sanitizeAiLogicNode(res.buyCondition) : null;
const sellCondition = action === 'build' ? sanitizeAiLogicNode(res.sellCondition) : null;
return {