전략편집기 수정

This commit is contained in:
Macbook
2026-05-25 17:56:25 +09:00
parent aac6454724
commit 02d14e4b2b
39 changed files with 1974 additions and 288 deletions
+83 -64
View File
@@ -32,10 +32,11 @@ import {
defaultStartMeta,
type StartCombineOp,
} from '../utils/strategyStartNodes';
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
import {
COMPOSITE_INDICATOR_ITEMS,
compositePeriodLabel,
} from '../utils/compositeIndicators';
loadPaletteItems,
type PaletteItem,
} from '../utils/strategyPaletteStorage';
import {
emptySignalFlowLayout,
loadStrategyFlowLayout,
@@ -135,6 +136,9 @@ export default function StrategyEditorPage({ theme }: Props) {
const [sellCondition, setSellCondition] = useState<LogicNode | null>(null);
const [signalTab, setSignalTab] = useState<'buy' | 'sell'>('buy');
const [rightTab, setRightTab] = useState<'indicators' | 'templates'>('indicators');
const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite'>('auxiliary');
const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary'));
const [compositePalette, setCompositePalette] = useState<PaletteItem[]>(() => loadPaletteItems('composite'));
const [paletteSearch, setPaletteSearch] = useState('');
const [selectedPaletteKey, setSelectedPaletteKey] = useState<string | null>(null);
const [stratName, setStratName] = useState('');
@@ -689,6 +693,19 @@ export default function StrategyEditorPage({ theme }: Props) {
else setCurrentRoot(mergeAtRoot(root, newNode, false));
}, [signalTab, DEF, currentRoot, setCurrentRoot, handleAddStartSection]);
const applyPaletteItem = useCallback((item: PaletteItem) => {
const composite = item.kind === 'composite';
const newNode = makeNode('indicator', item.value, signalTab, DEF, {
composite,
period: item.period,
leftPeriod: item.shortPeriod,
rightPeriod: item.longPeriod,
});
const root = currentRoot;
if (!root) setCurrentRoot(newNode);
else setCurrentRoot(mergeAtRoot(root, newNode, false));
}, [signalTab, DEF, currentRoot, setCurrentRoot]);
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
@@ -724,34 +741,16 @@ export default function StrategyEditorPage({ theme }: Props) {
{ type: 'indicator' as const, value: 'ICHIMOKU', label: 'Ichimoku', desc: '일목균형표', color: 'band' },
];
const indicatorItems = [
{ value: 'RSI', label: 'RSI', desc: '상대강도지수', color: 'ind' as const },
{ value: 'MACD', label: 'MACD', desc: '이동평균 수렴확산', color: 'ind' as const },
{ value: 'STOCHASTIC', label: 'Stochastic', desc: '스토캐스틱', color: 'ind' as const },
{ value: 'CCI', label: 'CCI', desc: '상품채널지수', color: 'ind' as const },
{ value: 'ADX', label: 'ADX', desc: '평균방향지수', color: 'ind' as const },
{ value: 'DMI', label: 'DMI', desc: '방향성 지표', color: 'ind' as const },
{ value: 'OBV', label: 'OBV', desc: '거래량 균형', color: 'ind' as const },
{ value: 'WILLIAMS_R', label: 'Williams %R', desc: '윌리엄스 %R', color: 'ind' as const },
{ value: 'TRIX', label: 'TRIX', desc: '삼중지수이동평균', color: 'ind' as const },
{ value: 'VOLUME_OSC', label: 'Volume OSC', desc: '거래량 오실레이터', color: 'ind' as const },
{ value: 'VR', label: 'VR', desc: '거래량 비율', color: 'ind' as const },
{ value: 'DISPARITY', label: '이격도', desc: 'Disparity', color: 'ind' as const },
{ value: 'PSYCHOLOGICAL', label: '심리도', desc: 'Psychological', color: 'ind' as const },
{ value: 'INVEST_PSYCHOLOGICAL', label: '투자심리도', desc: 'Invest PSY', color: 'ind' as const },
{ value: 'VOLUME', label: '거래량', desc: 'Volume', color: 'ind' as const },
];
const q = paletteSearch.trim().toLowerCase();
const match = (label: string, desc?: string) =>
!q || label.toLowerCase().includes(q) || (desc?.toLowerCase().includes(q) ?? false);
const paletteKey = (type: 'operator' | 'indicator' | 'composite', value: string) => `${type}:${value}`;
const selectPalette = (type: 'operator' | 'indicator' | 'composite', value: string) => {
setSelectedPaletteKey(paletteKey(type, value));
const paletteKey = (type: string, id: string) => `${type}:${id}`;
const selectPalette = (type: string, id: string) => {
setSelectedPaletteKey(paletteKey(type, id));
};
const isPaletteSelected = (type: 'operator' | 'indicator' | 'composite', value: string) =>
selectedPaletteKey === paletteKey(type, value);
const isPaletteSelected = (type: string, id: string) =>
selectedPaletteKey === paletteKey(type, id);
return (
<div className={`se-page se-page--${theme}`}>
@@ -1085,45 +1084,65 @@ export default function StrategyEditorPage({ theme }: Props) {
))}
</div>
</div>
<div className="se-palette-section se-palette-section--aux">
<h3></h3>
<div className="se-palette-grid se-palette-grid--3">
{indicatorItems.filter(i => match(i.label, i.desc)).map(item => (
<PaletteChip
key={item.value}
type="indicator"
value={item.value}
label={item.label}
desc={item.desc}
color={item.color}
period={getIndicatorPeriodLabel(item.value, DEF)}
selected={isPaletteSelected('indicator', item.value)}
onSelect={() => selectPalette('indicator', item.value)}
onAdd={() => applyPalette('indicator', item.value, item.label)}
/>
))}
</div>
</div>
<div className="se-palette-section se-palette-section--composite se-palette-section--scroll">
<h3></h3>
<div className="se-palette-grid se-palette-grid--3">
{COMPOSITE_INDICATOR_ITEMS.filter(i => match(i.label, i.desc)).map(item => (
<PaletteChip
key={`composite-${item.value}`}
type="indicator"
value={item.value}
label={item.label}
desc={item.desc}
color="composite"
composite
period={compositePeriodLabel(item.value, DEF)}
selected={isPaletteSelected('composite', item.value)}
onSelect={() => selectPalette('composite', item.value)}
onAdd={() => applyPalette('indicator', item.value, item.label, true)}
/>
))}
</div>
<div className="se-palette-subtabs">
<button
type="button"
className={`se-palette-subtab${indicatorSubTab === 'auxiliary' ? ' se-palette-subtab--on' : ''}`}
onClick={() => {
setIndicatorSubTab('auxiliary');
setSelectedPaletteKey(null);
}}
>
</button>
<button
type="button"
className={`se-palette-subtab${indicatorSubTab === 'composite' ? ' se-palette-subtab--on' : ''}`}
onClick={() => {
setIndicatorSubTab('composite');
setSelectedPaletteKey(null);
}}
>
</button>
</div>
{indicatorSubTab === 'auxiliary' ? (
<IndicatorPaletteTab
kind="auxiliary"
items={auxiliaryPalette}
onItemsChange={setAuxiliaryPalette}
def={DEF}
searchQuery={paletteSearch}
selectedItemId={
selectedPaletteKey?.startsWith('auxiliary:')
? selectedPaletteKey.slice('auxiliary:'.length)
: null
}
onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('auxiliary', id) : null)}
onAddToCanvas={item => {
selectPalette('auxiliary', item.id);
applyPaletteItem(item);
}}
/>
) : (
<IndicatorPaletteTab
kind="composite"
items={compositePalette}
onItemsChange={setCompositePalette}
def={DEF}
searchQuery={paletteSearch}
selectedItemId={
selectedPaletteKey?.startsWith('composite:')
? selectedPaletteKey.slice('composite:'.length)
: null
}
onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('composite', id) : null)}
onAddToCanvas={item => {
selectPalette('composite', item.id);
applyPaletteItem(item);
}}
/>
)}
</>
)}
{rightTab === 'templates' && (