ai 전략 agent 수정

This commit is contained in:
Macbook
2026-06-30 22:29:14 +09:00
parent 25cbadbffb
commit a3f65c26d0
5 changed files with 173 additions and 24 deletions
+46 -11
View File
@@ -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);
}, []);
@@ -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<Props> = ({ getContext, onApplyStrategy }) => {
const [turns, setTurns] = useState<Turn[]>([]);
const [input, setInput] = useState('');
@@ -61,11 +75,37 @@ const AiStrategyPanel: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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 => {
+1 -1
View File
@@ -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;
+3 -2
View File
@@ -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<StrategyAgentResult> {
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 {