33변곡 템플릿 추가

This commit is contained in:
Macbook
2026-05-24 13:03:39 +09:00
parent 03fab44cb3
commit 5685deb2c7
5 changed files with 173 additions and 43 deletions
+20 -15
View File
@@ -37,6 +37,11 @@ import {
saveEditorMode,
type StrategyEditorMode,
} from '../utils/strategyEditorModeStorage';
import {
getStrategyTemplates,
simpleTemplateToNode,
type StrategyTemplateDef,
} from '../utils/strategyPresets';
import PaletteChip from './strategyEditor/PaletteChip';
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
import '../styles/strategyEditor.css';
@@ -396,27 +401,24 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
else setCurrentRoot(mergeAtRoot(root, newNode, false));
}, [signalTab, DEF, currentRoot, setCurrentRoot]);
const templates = useMemo(() => [
{ ind: 'MA', cond: 'CROSS_UP', l: `MA${DEF.maLines[0]}`, r: `MA${DEF.maLines[1]}`, signal: 'buy' as const, label: 'MA 골든크로스 매수' },
{ ind: 'MA', cond: 'CROSS_DOWN', l: `MA${DEF.maLines[0]}`, r: `MA${DEF.maLines[1]}`, signal: 'sell' as const, label: 'MA 데드크로스 매도' },
{ ind: 'RSI', cond: 'LTE', l: 'RSI_VALUE', r: 'K_30', signal: 'buy' as const, label: 'RSI 과매도 매수' },
{ ind: 'RSI', cond: 'GTE', l: 'RSI_VALUE', r: 'K_70', signal: 'sell' as const, label: 'RSI 과매수 매도' },
{ ind: 'MACD', cond: 'CROSS_UP', l: 'MACD_LINE', r: 'SIGNAL_LINE', signal: 'buy' as const, label: 'MACD 상향 돌파' },
{ ind: 'MACD', cond: 'CROSS_DOWN', l: 'MACD_LINE', r: 'SIGNAL_LINE', signal: 'sell' as const, label: 'MACD 하향 돌파' },
], [DEF.maLines]);
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
const handleTemplate = (tmpl: typeof templates[0]) => {
const newNode: LogicNode = {
id: genId(), type: 'CONDITION',
condition: { indicatorType: tmpl.ind, conditionType: tmpl.cond, leftField: tmpl.l, rightField: tmpl.r, candleRange: 1 },
};
const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
if (tmpl.signal !== signalTab) switchSignalTab(tmpl.signal);
const root = tmpl.signal === 'buy' ? buyCondition : sellCondition;
const setRoot = tmpl.signal === 'buy' ? setBuyCondition : setSellCondition;
const root = tmpl.signal === 'buy' ? buyCondition : sellCondition;
if (tmpl.kind === 'composite') {
setRoot(tmpl.build());
showSnack(`"${tmpl.label}" 템플릿이 적용됐습니다`);
return;
}
const newNode = simpleTemplateToNode(tmpl);
if (!root) setRoot(newNode);
else setRoot(mergeAtRoot(root, newNode, false));
showSnack(`"${tmpl.label}" 추가됨`);
};
}, [signalTab, switchSignalTab, buyCondition, sellCondition, setBuyCondition, setSellCondition]);
const operators = [
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
@@ -705,6 +707,9 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
{templates.map((t, i) => (
<button key={i} type="button" className="se-template-item" onClick={() => handleTemplate(t)}>
<span className="se-template-label">{t.label}</span>
{'description' in t && t.description && (
<span className="se-template-desc">{t.description}</span>
)}
<span className={`se-template-signal se-template-signal--${t.signal}`}>{t.signal === 'buy' ? '매수' : '매도'}</span>
</button>
))}
+20 -20
View File
@@ -13,6 +13,11 @@ import {
type StrategyDto as ApiStrategyDto,
} from '../utils/backendApi';
import { getIndicatorDef, getHLineLabel } from '../utils/indicatorRegistry';
import {
getStrategyTemplates,
simpleTemplateToNode,
type StrategyTemplateDef,
} from '../utils/strategyPresets';
// ─── 타입 ─────────────────────────────────────────────────────────────────────
type LogicNodeType = 'AND' | 'OR' | 'NOT' | 'CONDITION';
@@ -1184,14 +1189,19 @@ export const StrategyPage: React.FC<Props> = ({ activeIndicators = [] }) => {
};
// ── 템플릿 선택 ─────────────────────────────────────────────────────────────
const handleTemplateSelect = (tmpl: { ind: string; cond: string; l: string; r: string; signal: 'buy'|'sell'; label: string }) => {
const newNode: LogicNode = {
id: genId(), type: 'CONDITION',
condition: { indicatorType: tmpl.ind, conditionType: tmpl.cond, leftField: tmpl.l, rightField: tmpl.r, candleRange: 1 },
};
const handleTemplateSelect = (tmpl: StrategyTemplateDef) => {
if (tmpl.signal !== currentTab) setCurrentTab(tmpl.signal);
const root = tmpl.signal === 'buy' ? buyCondition : sellCondition;
const setRoot = tmpl.signal === 'buy' ? setBuyCondition : setSellCondition;
const root = tmpl.signal === 'buy' ? buyCondition : sellCondition;
if (tmpl.kind === 'composite') {
setRoot(tmpl.build());
setRightTab('preview');
showSnack(`"${tmpl.label}" 템플릿이 적용됐습니다`);
return;
}
const newNode = simpleTemplateToNode(tmpl);
if (!root) { setRoot(newNode); }
else { setRoot(mergeAtRoot(root, newNode, false)); }
setRightTab('preview');
@@ -1217,20 +1227,7 @@ export const StrategyPage: React.FC<Props> = ({ activeIndicators = [] }) => {
'TRIX:TRIX', 'VOLUME_OSC:VolumeOSC', 'VR:VR', 'DISPARITY:이격도', 'VOLUME:거래량',
].map(s => { const [v,l] = s.split(':'); return { type:'indicator' as const, value:v, label:l, color:'ind' }; });
const templates = [
{ ind:'MA', cond:'CROSS_UP', l:`MA${DEF.maLines[0]}`, r:`MA${DEF.maLines[1]}`, signal:'buy' as const, label:'MA 골든크로스 매수' },
{ ind:'MA', cond:'CROSS_DOWN', l:`MA${DEF.maLines[0]}`, r:`MA${DEF.maLines[1]}`, signal:'sell' as const, label:'MA 데드크로스 매도' },
{ ind:'RSI', cond:'LTE', l:'RSI_VALUE', r:'OVERSOLD_30', signal:'buy' as const, label:'RSI 과매도 매수' },
{ ind:'RSI', cond:'GTE', l:'RSI_VALUE', r:'OVERBOUGHT_70', signal:'sell' as const, label:'RSI 과매수 매도' },
{ ind:'MACD', cond:'CROSS_UP', l:'MACD_LINE', r:'SIGNAL_LINE', signal:'buy' as const, label:'MACD 시그널 상향 돌파' },
{ ind:'MACD', cond:'CROSS_DOWN', l:'MACD_LINE', r:'SIGNAL_LINE', signal:'sell' as const, label:'MACD 시그널 하향 돌파' },
{ ind:'BOLLINGER', cond:'CROSS_UP', l:'CLOSE_PRICE', r:'LOWER_BAND', signal:'buy' as const, label:'볼린저 하단 돌파 매수' },
{ ind:'BOLLINGER', cond:'CROSS_UP', l:'CLOSE_PRICE', r:'UPPER_BAND', signal:'sell' as const, label:'볼린저 상단 돌파 매도' },
{ ind:'CCI', cond:'CROSS_UP', l:'CCI_VALUE', r:'OVERSOLD_NEG100', signal:'buy' as const, label:'CCI 과매도 반등' },
{ ind:'PSYCHOLOGICAL', cond:'CROSS_UP', l:'PSY_VALUE', r:'OVERBOUGHT_75', signal:'buy' as const, label:'심리도 상향 돌파' },
{ ind:'STOCHASTIC', cond:'CROSS_UP', l:'STOCH_K', r:'STOCH_D', signal:'buy' as const, label:'스토캐스틱 골든크로스' },
{ ind:'ADX', cond:'CROSS_UP', l:'ADX_VALUE', r:'ADX_25', signal:'buy' as const, label:'ADX 중앙선 상향 돌파' },
];
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
const currentRoot = getCurrent();
@@ -1548,6 +1545,9 @@ export const StrategyPage: React.FC<Props> = ({ activeIndicators = [] }) => {
{t.signal === 'buy' ? '매수' : '매도'}
</span>
<span className="sp-tmpl-name">{t.label}</span>
{'description' in t && t.description && (
<span className="sp-tmpl-desc">{t.description}</span>
)}
</div>
))}
</div>