211 lines
8.0 KiB
TypeScript
211 lines
8.0 KiB
TypeScript
/**
|
|
* 전략 평가 — 우측 패널 전략지표 (전략편집기 지표 탭과 동일)
|
|
*/
|
|
import React, { useMemo, useState } from 'react';
|
|
import PaletteChip from '../strategyEditor/PaletteChip';
|
|
import IndicatorPaletteTab from '../strategyEditor/IndicatorPaletteTab';
|
|
import SidewaysFilterPaletteTab from '../strategyEditor/SidewaysFilterPaletteTab';
|
|
import { getIndicatorPeriodLabel } from '../../utils/strategyFlowLayout';
|
|
import {
|
|
loadPaletteItems,
|
|
type PaletteItem,
|
|
} from '../../utils/strategyPaletteStorage';
|
|
import { loadSidewaysPaletteItems } from '../../utils/sidewaysFilterPaletteStorage';
|
|
import type { DefType } from '../../utils/strategyEditorShared';
|
|
import type { useStrategyEvaluationEditor } from '../../hooks/useStrategyEvaluationEditor';
|
|
|
|
type EditorApi = Pick<
|
|
ReturnType<typeof useStrategyEvaluationEditor>,
|
|
'def' | 'applyPalette' | 'applyPaletteItem' | 'applySidewaysFilter'
|
|
>;
|
|
|
|
interface Props {
|
|
editor: EditorApi;
|
|
}
|
|
|
|
const OPERATORS = [
|
|
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
|
{ type: 'operator' as const, value: 'OR', label: 'OR', color: 'logic-or' },
|
|
{ type: 'operator' as const, value: 'NOT', label: 'NOT', color: 'logic-not' },
|
|
];
|
|
|
|
const MA_BAND_ITEMS = [
|
|
{ type: 'indicator' as const, value: 'MA', label: 'MA', desc: '이동평균', color: 'band' },
|
|
{ type: 'indicator' as const, value: 'EMA', label: 'EMA', desc: '지수이동평균', color: 'band' },
|
|
{ type: 'indicator' as const, value: 'BOLLINGER', label: 'Bollinger', desc: '볼린저밴드', color: 'band' },
|
|
{ type: 'indicator' as const, value: 'DONCHIAN', label: 'Donchian', desc: '돈치안 채널', color: 'band' },
|
|
{ type: 'indicator' as const, value: 'ICHIMOKU', label: 'Ichimoku', desc: '일목균형표', color: 'band' },
|
|
];
|
|
|
|
function paletteKey(type: string, id: string) {
|
|
return `${type}:${id}`;
|
|
}
|
|
|
|
const StrategyEvaluationIndicatorPalettePanel: React.FC<Props> = ({ editor }) => {
|
|
const { def, applyPalette, applyPaletteItem, applySidewaysFilter } = editor;
|
|
|
|
const [paletteSearch, setPaletteSearch] = useState('');
|
|
const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite' | 'range'>('auxiliary');
|
|
const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary'));
|
|
const [compositePalette, setCompositePalette] = useState<PaletteItem[]>(() => loadPaletteItems('composite'));
|
|
const [sidewaysPalette, setSidewaysPalette] = useState(() => loadSidewaysPaletteItems());
|
|
const [selectedPaletteKey, setSelectedPaletteKey] = useState<string | null>(null);
|
|
|
|
const q = paletteSearch.trim().toLowerCase();
|
|
const match = (label: string, desc?: string) =>
|
|
!q || label.toLowerCase().includes(q) || (desc?.toLowerCase().includes(q) ?? false);
|
|
|
|
const isPaletteSelected = (type: string, id: string) =>
|
|
selectedPaletteKey === paletteKey(type, id);
|
|
|
|
const selectPalette = (type: string, id: string) => {
|
|
setSelectedPaletteKey(paletteKey(type, id));
|
|
};
|
|
|
|
const filteredMaBand = useMemo(
|
|
() => MA_BAND_ITEMS.filter(i => match(i.label, i.desc)),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[q],
|
|
);
|
|
|
|
return (
|
|
<div className="seval-indicator-palette se-palette-panel">
|
|
<input
|
|
className="se-palette-search"
|
|
placeholder="지표 검색 (RSI, MACD…)"
|
|
value={paletteSearch}
|
|
onChange={e => setPaletteSearch(e.target.value)}
|
|
/>
|
|
<div className="se-palette-section se-palette-section--logic">
|
|
<h3>시작 · 논리</h3>
|
|
<div className="se-palette-grid se-palette-grid--3">
|
|
<PaletteChip
|
|
type="start"
|
|
value="START"
|
|
label="START"
|
|
desc="시간봉 시작점"
|
|
color="logic-start"
|
|
selected={selectedPaletteKey === 'start:START'}
|
|
onSelect={() => setSelectedPaletteKey('start:START')}
|
|
onAdd={() => applyPalette('start', 'START')}
|
|
/>
|
|
{OPERATORS.map(op => (
|
|
<PaletteChip
|
|
key={op.value}
|
|
{...op}
|
|
selected={isPaletteSelected(op.type, op.value)}
|
|
onSelect={() => selectPalette(op.type, op.value)}
|
|
onAdd={() => applyPalette(op.type, op.value)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{filteredMaBand.length > 0 && (
|
|
<div className="se-palette-section se-palette-section--band">
|
|
<h3>밴드 · 추세</h3>
|
|
<div className="se-palette-grid se-palette-grid--3">
|
|
{filteredMaBand.map(item => (
|
|
<PaletteChip
|
|
key={item.value}
|
|
{...item}
|
|
period={getIndicatorPeriodLabel(item.value, def as DefType)}
|
|
selected={isPaletteSelected(item.type, item.value)}
|
|
onSelect={() => selectPalette(item.type, item.value)}
|
|
onAdd={() => applyPalette(item.type, item.value)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="se-palette-subtabs">
|
|
<button
|
|
type="button"
|
|
className={`se-palette-subtab se-palette-subtab--aux${indicatorSubTab === 'auxiliary' ? ' se-palette-subtab--on' : ''}`}
|
|
onClick={() => {
|
|
setIndicatorSubTab('auxiliary');
|
|
setSelectedPaletteKey(null);
|
|
}}
|
|
>
|
|
보조지표
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`se-palette-subtab se-palette-subtab--composite${indicatorSubTab === 'composite' ? ' se-palette-subtab--on' : ''}`}
|
|
onClick={() => {
|
|
setIndicatorSubTab('composite');
|
|
setSelectedPaletteKey(null);
|
|
}}
|
|
>
|
|
복합지표
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`se-palette-subtab se-palette-subtab--range${indicatorSubTab === 'range' ? ' se-palette-subtab--on' : ''}`}
|
|
onClick={() => {
|
|
setIndicatorSubTab('range');
|
|
setSelectedPaletteKey(null);
|
|
}}
|
|
>
|
|
횡보필터
|
|
</button>
|
|
</div>
|
|
{indicatorSubTab === 'auxiliary' ? (
|
|
<IndicatorPaletteTab
|
|
kind="auxiliary"
|
|
items={auxiliaryPalette}
|
|
onItemsChange={setAuxiliaryPalette}
|
|
def={def as DefType}
|
|
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);
|
|
}}
|
|
/>
|
|
) : indicatorSubTab === 'composite' ? (
|
|
<IndicatorPaletteTab
|
|
kind="composite"
|
|
items={compositePalette}
|
|
onItemsChange={setCompositePalette}
|
|
def={def as DefType}
|
|
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);
|
|
}}
|
|
/>
|
|
) : (
|
|
<SidewaysFilterPaletteTab
|
|
def={def as DefType}
|
|
items={sidewaysPalette}
|
|
onItemsChange={setSidewaysPalette}
|
|
searchQuery={paletteSearch}
|
|
selectedItemId={
|
|
selectedPaletteKey?.startsWith('range:')
|
|
? selectedPaletteKey.slice('range:'.length)
|
|
: null
|
|
}
|
|
onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('range', id) : null)}
|
|
onAddToCanvas={item => {
|
|
selectPalette('range', item.id);
|
|
applySidewaysFilter(item.id);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StrategyEvaluationIndicatorPalettePanel;
|