전략편집기 횡보구간 컨트롤 추가
This commit is contained in:
@@ -45,6 +45,8 @@ import {
|
||||
type StartCombineOp,
|
||||
} from '../utils/strategyStartNodes';
|
||||
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
|
||||
import SidewaysFilterPaletteTab from './strategyEditor/SidewaysFilterPaletteTab';
|
||||
import { buildSidewaysFilterNode } from '../utils/sidewaysFilterPalette';
|
||||
import {
|
||||
loadPaletteItems,
|
||||
type PaletteItem,
|
||||
@@ -99,7 +101,16 @@ import {
|
||||
strategyDtoToListExportItem,
|
||||
} from '../utils/strategyImportExport';
|
||||
import PaletteChip from './strategyEditor/PaletteChip';
|
||||
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
||||
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
||||
import {
|
||||
clampPanelSize,
|
||||
readStoredBool,
|
||||
readStoredSize,
|
||||
storeBool,
|
||||
storeSize,
|
||||
usePanelResize,
|
||||
useRightPanelResize,
|
||||
} from './strategyEditor/usePanelResize';
|
||||
import StrategyDescriptionModal from './strategyEditor/StrategyDescriptionModal';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { MarketSearchPanel } from './MarketSearchPanel';
|
||||
@@ -120,10 +131,21 @@ const BACKTEST_FOCUS_KEY = 'backtest_focus_id';
|
||||
const LEFT_PANEL_MIN = 220;
|
||||
const LEFT_PANEL_MAX = 520;
|
||||
const LEFT_PANEL_DEFAULT = 280;
|
||||
const RIGHT_PANEL_MIN = 380;
|
||||
const RIGHT_PANEL_MAX = 560;
|
||||
const RIGHT_PANEL_DEFAULT = 380;
|
||||
const TERMINAL_MIN = 88;
|
||||
const TERMINAL_MAX = 420;
|
||||
const TERMINAL_DEFAULT = 140;
|
||||
|
||||
function readRightPanelWidth(): number {
|
||||
return clampPanelSize(
|
||||
readStoredSize('se-right-width', RIGHT_PANEL_DEFAULT),
|
||||
RIGHT_PANEL_MIN,
|
||||
RIGHT_PANEL_MAX,
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
theme: Theme;
|
||||
onNavigateToBacktest?: () => void;
|
||||
@@ -189,7 +211,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
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 [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite' | 'range'>('auxiliary');
|
||||
const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary'));
|
||||
const [compositePalette, setCompositePalette] = useState<PaletteItem[]>(() => loadPaletteItems('composite'));
|
||||
const [paletteSearch, setPaletteSearch] = useState('');
|
||||
@@ -224,8 +246,11 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
const qbMarketBtnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
|
||||
const [rightWidth, setRightWidth] = useState(() => readRightPanelWidth());
|
||||
const [rightOpen, setRightOpen] = useState(() => readStoredBool('se-right-open', true));
|
||||
const [terminalHeight, setTerminalHeight] = useState(() => readStoredSize('se-terminal-height', TERMINAL_DEFAULT));
|
||||
const leftWidthRef = useRef(leftWidth);
|
||||
const rightWidthRef = useRef(rightWidth);
|
||||
const terminalHeightRef = useRef(terminalHeight);
|
||||
const [buyLayout, setBuyLayout] = useState(() => normalizeTabLayoutState(emptySignalFlowLayout()));
|
||||
const [sellLayout, setSellLayout] = useState(() => normalizeTabLayoutState(emptySignalFlowLayout()));
|
||||
@@ -246,6 +271,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
const layoutPersistReadyRef = useRef(false);
|
||||
const STRATEGY_AUTOSAVE_MS = 350;
|
||||
leftWidthRef.current = leftWidth;
|
||||
rightWidthRef.current = rightWidth;
|
||||
terminalHeightRef.current = terminalHeight;
|
||||
|
||||
const [layoutSeedKey, setLayoutSeedKey] = useState('draft:buy:0');
|
||||
@@ -260,6 +286,22 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
v => storeSize('se-left-width', v),
|
||||
);
|
||||
|
||||
const onRightSplitter = useRightPanelResize(
|
||||
setRightWidth,
|
||||
() => rightWidthRef.current,
|
||||
RIGHT_PANEL_MIN,
|
||||
RIGHT_PANEL_MAX,
|
||||
v => storeSize('se-right-width', v),
|
||||
);
|
||||
|
||||
const toggleRightPanel = useCallback(() => {
|
||||
setRightOpen(prev => {
|
||||
const next = !prev;
|
||||
storeBool('se-right-open', next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onTerminalSplitter = usePanelResize(
|
||||
'horizontal',
|
||||
setTerminalHeight,
|
||||
@@ -271,8 +313,9 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
|
||||
const bodyStyle = useMemo(() => ({
|
||||
'--se-left-width': `${leftWidth}px`,
|
||||
'--se-right-width': `${rightWidth}px`,
|
||||
'--se-terminal-height': `${terminalHeight}px`,
|
||||
}) as React.CSSProperties, [leftWidth, terminalHeight]);
|
||||
}) as React.CSSProperties, [leftWidth, rightWidth, terminalHeight]);
|
||||
|
||||
const showSnack = (msg: string, ok = true) => {
|
||||
setSnack({ msg, ok });
|
||||
@@ -1021,6 +1064,15 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
scheduleStrategyPersist();
|
||||
}, [signalTab, DEF, currentRoot, setCurrentRoot, scheduleStrategyPersist]);
|
||||
|
||||
const applySidewaysFilter = useCallback((filterId: string) => {
|
||||
const newNode = buildSidewaysFilterNode(filterId, DEF);
|
||||
if (!newNode) return;
|
||||
const root = currentRoot;
|
||||
if (!root) setCurrentRoot(newNode);
|
||||
else setCurrentRoot(mergeAtRoot(root, newNode, false));
|
||||
scheduleStrategyPersist();
|
||||
}, [DEF, currentRoot, setCurrentRoot, scheduleStrategyPersist]);
|
||||
|
||||
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
|
||||
|
||||
const templateRows = useMemo(() => {
|
||||
@@ -1645,7 +1697,24 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside className="se-right">
|
||||
{rightOpen && (
|
||||
<div
|
||||
className="se-splitter se-splitter--v"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="우측 패널 너비 조절"
|
||||
onPointerDown={onRightSplitter}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={`se-side-wrap se-side-wrap--right${rightOpen ? ' se-side-wrap--open' : ''}`}>
|
||||
<SePanelCollapseHandle
|
||||
side="right"
|
||||
open={rightOpen}
|
||||
onToggle={toggleRightPanel}
|
||||
label="지표 패널"
|
||||
/>
|
||||
<aside className={`se-right se-right--collapsible${rightOpen ? ' se-right--collapsible--open' : ''}`}>
|
||||
<div className="se-palette-panel">
|
||||
<div className="se-right-tabs">
|
||||
<button type="button" className={rightTab === 'indicators' ? 'se-right-tab se-right-tab--on' : 'se-right-tab'} onClick={() => setRightTab('indicators')}>지표</button>
|
||||
@@ -1702,7 +1771,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
<div className="se-palette-subtabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`se-palette-subtab${indicatorSubTab === 'auxiliary' ? ' se-palette-subtab--on' : ''}`}
|
||||
className={`se-palette-subtab se-palette-subtab--aux${indicatorSubTab === 'auxiliary' ? ' se-palette-subtab--on' : ''}`}
|
||||
onClick={() => {
|
||||
setIndicatorSubTab('auxiliary');
|
||||
setSelectedPaletteKey(null);
|
||||
@@ -1712,7 +1781,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`se-palette-subtab${indicatorSubTab === 'composite' ? ' se-palette-subtab--on' : ''}`}
|
||||
className={`se-palette-subtab se-palette-subtab--composite${indicatorSubTab === 'composite' ? ' se-palette-subtab--on' : ''}`}
|
||||
onClick={() => {
|
||||
setIndicatorSubTab('composite');
|
||||
setSelectedPaletteKey(null);
|
||||
@@ -1720,6 +1789,16 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
>
|
||||
복합지표
|
||||
</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
|
||||
@@ -1739,7 +1818,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
applyPaletteItem(item);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
) : indicatorSubTab === 'composite' ? (
|
||||
<IndicatorPaletteTab
|
||||
kind="composite"
|
||||
items={compositePalette}
|
||||
@@ -1757,6 +1836,21 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
applyPaletteItem(item);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SidewaysFilterPaletteTab
|
||||
def={DEF}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -1821,6 +1915,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user