From cd5502d302c36e5b5279159df6edb1e66541e56c Mon Sep 17 00:00:00 2001 From: Macbook Date: Wed, 27 May 2026 09:53:19 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EC=A7=80=ED=91=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EA=B0=92=20=EB=B3=80=EA=B2=BD=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8E=B8=EC=A7=91=EA=B8=B0=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.css | 24 ++++ frontend/src/App.tsx | 4 + .../components/IndicatorMainDefaultsPanel.tsx | 127 +++++++++++++----- frontend/src/components/SettingsPage.tsx | 47 ++++++- .../src/components/StrategyEditorPage.tsx | 56 ++++---- .../strategyEditor/IndicatorPaletteTab.tsx | 6 +- .../strategyEditor/PaletteItemModal.tsx | 2 +- frontend/src/hooks/useIndicatorSettings.ts | 54 +++++++- frontend/src/utils/conditionPeriods.ts | 18 +-- .../src/utils/indicatorDefaultsFingerprint.ts | 17 +++ frontend/src/utils/strategyDefSync.ts | 16 +-- frontend/src/utils/strategyEditorShared.tsx | 15 ++- frontend/src/utils/strategyPaletteStorage.ts | 12 +- 13 files changed, 288 insertions(+), 110 deletions(-) create mode 100644 frontend/src/utils/indicatorDefaultsFingerprint.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index 1a4bd7f..5780a6e 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -8701,6 +8701,30 @@ html.theme-blue { background: var(--bg2); flex-shrink: 0; } +.stg-content-header-text { + flex: 1; + min-width: 0; +} +.stg-content-header-actions { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; + margin-left: auto; +} +.stg-ind-save-msg { + font-size: 12px; + color: var(--green, #4caf50); + margin: 0; +} +.stg-ind-save-msg--inline { + max-width: 160px; + text-align: right; + line-height: 1.35; +} +.stg-ind-save-msg--err { + color: var(--red, #ef5350); +} .stg-content-icon { display: flex; align-items: center; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 48f904f..023fabb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -158,6 +158,8 @@ function App() { saveParams: saveIndicatorParams, getVisualConfig: getIndicatorVisual, saveVisual: saveIndicatorVisual, + saveAllIndicatorDefaults, + settingsRevision: indicatorSettingsRevision, } = useIndicatorSettings(sessionKey); const getParams = getIndicatorParams; const saveParams = saveIndicatorParams; @@ -1704,6 +1706,8 @@ function App() { saveIndicatorParams={saveIndicatorParams} getIndicatorVisual={getIndicatorVisual} saveIndicatorVisual={saveIndicatorVisual} + saveAllIndicatorDefaults={saveAllIndicatorDefaults} + indicatorSettingsRevision={indicatorSettingsRevision} chartIndicators={bulkSettingsIndicators} onAddIndicatorToChart={handleAddIndicator} onRemoveIndicatorFromChart={handleRemoveByType} diff --git a/frontend/src/components/IndicatorMainDefaultsPanel.tsx b/frontend/src/components/IndicatorMainDefaultsPanel.tsx index 6d72b4b..30ff395 100644 --- a/frontend/src/components/IndicatorMainDefaultsPanel.tsx +++ b/frontend/src/components/IndicatorMainDefaultsPanel.tsx @@ -5,7 +5,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import type { IndicatorConfig } from '../types'; import type { PlotDef, HLineDef } from '../utils/indicatorRegistry'; import { buildMainIndicatorConfig } from '../utils/indicatorMainConfig'; -import { indicatorTypesOnChart } from '../utils/indicatorMainConfig'; +import { indicatorDefaultsFingerprint } from '../utils/indicatorDefaultsFingerprint'; import IndicatorSettingsForm from './IndicatorSettingsForm'; import { resetConfigToDefaults } from '../utils/indicatorSettingsEditor'; import type { IchimokuCloudColors } from '../utils/ichimokuConfig'; @@ -18,6 +18,13 @@ import IndicatorSettingsListSearch from './IndicatorSettingsListSearch'; import IndicatorSettingsListRow from './IndicatorSettingsListRow'; import { MAIN_INDICATOR_TYPES } from '../utils/indicatorMainTab'; +export interface IndicatorSaveUiState { + save: () => Promise; + dirty: boolean; + saving: boolean; + saveMessage: string | null; +} + export interface IndicatorMainDefaultsPanelProps { chartIndicators: IndicatorConfig[]; onAddToChart: (type: string, config?: IndicatorConfig) => void; @@ -35,32 +42,65 @@ export interface IndicatorMainDefaultsPanelProps { hlines?: HLineDef[], cloudColors?: IchimokuCloudColors, ) => void; -} - -function persistDefaults( - updated: IndicatorConfig, - saveParams: IndicatorMainDefaultsPanelProps['saveParams'], - saveVisual: IndicatorMainDefaultsPanelProps['saveVisual'], -) { - saveParams(updated.type, updated.params); - saveVisual(updated.type, updated.plots, updated.hlines, updated.cloudColors); + /** DB에 보조지표 기본값 일괄 저장 */ + saveAllDefaults?: (configs: Record) => Promise; + /** 상단 저장 버튼 상태 (SettingsPage 헤더 연동) */ + onSaveUiState?: (state: IndicatorSaveUiState | null) => void; + /** DB 로드 완료·저장 후 재로드 트리거 */ + settingsRevision?: number; } const ALL_TYPES = getSettingsIndicatorTypes(); +function buildDefaultsMap( + getParams: IndicatorMainDefaultsPanelProps['getParams'], + getVisualConfig: IndicatorMainDefaultsPanelProps['getVisualConfig'], +): Record { + const next: Record = {}; + for (const type of ALL_TYPES) { + const cfg = buildMainIndicatorConfig(type, [], getParams, getVisualConfig); + if (cfg) next[type] = cfg; + } + return next; +} + +function fingerprintMap(map: Record): string { + return ALL_TYPES.map(t => map[t] ? indicatorDefaultsFingerprint(map[t]) : '').join('|'); +} + const IndicatorMainDefaultsPanel: React.FC = ({ chartIndicators, onAddToChart, onRemoveFromChart, getParams, - saveParams, getVisualConfig, - saveVisual, + saveAllDefaults, + onSaveUiState, + settingsRevision = 0, }) => { - const onChartTypes = useMemo(() => indicatorTypesOnChart(chartIndicators), [chartIndicators]); + const onChartTypes = useMemo(() => new Set(chartIndicators.map(i => i.type)), [chartIndicators]); const [expandedTypes, setExpandedTypes] = useState>(() => new Set()); const [configs, setConfigs] = useState>({}); + const [baselineFp, setBaselineFp] = useState(''); const [search, setSearch] = useState(''); + const [saving, setSaving] = useState(false); + const [saveMessage, setSaveMessage] = useState(null); + + const reloadFromDb = useCallback(() => { + const next = buildDefaultsMap(getParams, getVisualConfig); + setConfigs(next); + setBaselineFp(fingerprintMap(next)); + setSaveMessage(null); + }, [getParams, getVisualConfig]); + + useEffect(() => { + reloadFromDb(); + }, [reloadFromDb, settingsRevision]); + + const dirty = useMemo( + () => baselineFp !== '' && fingerprintMap(configs) !== baselineFp, + [configs, baselineFp], + ); const filtered = useMemo( () => filterSettingsIndicatorTypes(ALL_TYPES, search), @@ -71,15 +111,6 @@ const IndicatorMainDefaultsPanel: React.FC = ({ [filtered], ); - useEffect(() => { - const next: Record = {}; - for (const type of ALL_TYPES) { - const cfg = buildMainIndicatorConfig(type, chartIndicators, getParams, getVisualConfig); - if (cfg) next[type] = cfg; - } - setConfigs(next); - }, [chartIndicators, getParams, getVisualConfig]); - const toggleExpand = useCallback((type: string) => { setExpandedTypes(prev => { const next = new Set(prev); @@ -91,6 +122,7 @@ const IndicatorMainDefaultsPanel: React.FC = ({ const handleConfigChange = useCallback((type: string, updated: IndicatorConfig) => { setConfigs(prev => ({ ...prev, [type]: updated })); + setSaveMessage(null); }, []); const handleToggleChart = useCallback((type: string, enabled: boolean) => { @@ -105,22 +137,52 @@ const IndicatorMainDefaultsPanel: React.FC = ({ const base = configs[type]; if (!base) return; const reset = resetConfigToDefaults(base); - persistDefaults(reset, saveParams, saveVisual); handleConfigChange(type, reset); - }, [configs, saveParams, saveVisual, handleConfigChange]); + }, [configs, handleConfigChange]); const handleResetAll = useCallback(() => { - if (!window.confirm('목록에 있는 모든 보조지표 기본값을 초기화할까요?')) return; + if (!window.confirm('목록에 있는 모든 보조지표 기본값을 초기화할까요? (저장 버튼을 눌러야 DB에 반영됩니다)')) return; const next: Record = {}; for (const type of ALL_TYPES) { const base = configs[type] ?? buildMainIndicatorConfig(type, [], getParams, getVisualConfig); if (!base) continue; - const reset = resetConfigToDefaults(base); - next[type] = reset; - persistDefaults(reset, saveParams, saveVisual); + next[type] = resetConfigToDefaults(base); } setConfigs(next); - }, [configs, getParams, getVisualConfig, saveParams, saveVisual]); + setSaveMessage(null); + }, [configs, getParams, getVisualConfig]); + + const handleSaveAll = useCallback(async () => { + if (!saveAllDefaults) { + setSaveMessage('저장 API를 사용할 수 없습니다.'); + return; + } + if (!dirty) return; + setSaving(true); + setSaveMessage(null); + try { + await saveAllDefaults(configs); + const fp = fingerprintMap(configs); + setBaselineFp(fp); + setSaveMessage('저장되었습니다.'); + } catch (err) { + console.error('[IndicatorMainDefaultsPanel] save failed', err); + setSaveMessage('저장에 실패했습니다. 다시 시도해 주세요.'); + } finally { + setSaving(false); + } + }, [saveAllDefaults, configs, dirty]); + + useEffect(() => { + if (!onSaveUiState) return; + onSaveUiState({ + save: handleSaveAll, + dirty, + saving, + saveMessage, + }); + return () => onSaveUiState(null); + }, [onSaveUiState, handleSaveAll, dirty, saving, saveMessage]); const renderRows = (types: string[]) => types.map(type => { const cfg = configs[type]; @@ -135,10 +197,7 @@ const IndicatorMainDefaultsPanel: React.FC = ({ variant="settings" onToggleExpand={() => toggleExpand(type)} onToggleEnabled={on => handleToggleChart(type, on)} - onChange={updated => { - persistDefaults(updated, saveParams, saveVisual); - handleConfigChange(type, updated); - }} + onChange={updated => handleConfigChange(type, updated)} onRowDefaults={() => handleRowDefaults(type)} /> ); @@ -149,7 +208,7 @@ const IndicatorMainDefaultsPanel: React.FC = ({

지표 추가 팝업 Main 탭 {MAIN_INDICATOR_TYPES.length}종을 상단에 두었고, 그 아래에 등록된 모든 보조지표가 있습니다. - 우측 스위치로 차트에 바로 추가·제거할 수 있으며, 파라미터·색상 변경은 DB 기본값에 자동 저장됩니다. + 우측 스위치로 차트에 바로 추가·제거할 수 있습니다. 파라미터·색상을 변경한 뒤 상단 저장 버튼을 눌러 DB에 반영하세요.

+
+ )} {/* 설정 패널 */} @@ -1649,11 +1687,6 @@ const SettingsPage: React.FC = ({ )} - {active === 'indicators' && ( -
- 보조지표 기본값은 항목을 수정할 때마다 자동으로 저장됩니다. -
- )} ); diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx index 56eb1d6..58f7867 100644 --- a/frontend/src/components/StrategyEditorPage.tsx +++ b/frontend/src/components/StrategyEditorPage.tsx @@ -254,7 +254,29 @@ export default function StrategyEditorPage({ theme }: Props) { const orphanTotal = buyOrphans.length + sellOrphans.length; - /** 보조지표 설정 변경 시 오버라이드되지 않은 조건 노드 기준값 동기화 */ + const layoutStrategyKey = selectedId != null ? String(selectedId) : 'draft'; + + const persistFlowLayout = useCallback((strategyKey: string) => { + saveStrategyFlowLayout(strategyKey, { + buy: { ...buyLayoutRef.current, orphans: buyOrphans }, + sell: { ...sellLayoutRef.current, orphans: sellOrphans }, + }); + }, [buyOrphans, sellOrphans]); + + const schedulePersistFlowLayout = useCallback((strategyKey: string) => { + if (persistLayoutTimerRef.current != null) window.clearTimeout(persistLayoutTimerRef.current); + persistLayoutTimerRef.current = window.setTimeout(() => { + persistLayoutTimerRef.current = null; + persistFlowLayout(strategyKey); + }, 150); + }, [persistFlowLayout]); + + const bumpLayoutSeed = useCallback((strategyKey: string, tab: 'buy' | 'sell') => { + layoutRevisionRef.current += 1; + setLayoutSeedKey(`${strategyKey}:${tab}:${layoutRevisionRef.current}`); + }, []); + + /** 보조지표 설정 변경 시 오버라이드되지 않은 조건 노드·캔버스 기준값 동기화 */ useEffect(() => { if (settingsRevision <= settingsSyncRef.current) return; settingsSyncRef.current = settingsRevision; @@ -282,29 +304,8 @@ export default function StrategyEditorPage({ theme }: Props) { ...prev, extraRoots: syncExtraRoots(prev.extraRoots ?? {}, 'sell'), })); - }, [settingsRevision, DEF]); - - const layoutStrategyKey = selectedId != null ? String(selectedId) : 'draft'; - - const persistFlowLayout = useCallback((strategyKey: string) => { - saveStrategyFlowLayout(strategyKey, { - buy: { ...buyLayoutRef.current, orphans: buyOrphans }, - sell: { ...sellLayoutRef.current, orphans: sellOrphans }, - }); - }, [buyOrphans, sellOrphans]); - - const schedulePersistFlowLayout = useCallback((strategyKey: string) => { - if (persistLayoutTimerRef.current != null) window.clearTimeout(persistLayoutTimerRef.current); - persistLayoutTimerRef.current = window.setTimeout(() => { - persistLayoutTimerRef.current = null; - persistFlowLayout(strategyKey); - }, 150); - }, [persistFlowLayout]); - - const bumpLayoutSeed = useCallback((strategyKey: string, tab: 'buy' | 'sell') => { - layoutRevisionRef.current += 1; - setLayoutSeedKey(`${strategyKey}:${tab}:${layoutRevisionRef.current}`); - }, []); + bumpLayoutSeed(layoutStrategyKey, signalTab); + }, [settingsRevision, DEF, layoutStrategyKey, signalTab, bumpLayoutSeed]); const resetFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => { const emptyBuy = emptySignalFlowLayout(); @@ -738,12 +739,7 @@ export default function StrategyEditorPage({ theme }: Props) { 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 newNode = makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined); const root = currentRoot; if (!root) setCurrentRoot(newNode); else setCurrentRoot(mergeAtRoot(root, newNode, false)); diff --git a/frontend/src/components/strategyEditor/IndicatorPaletteTab.tsx b/frontend/src/components/strategyEditor/IndicatorPaletteTab.tsx index 79cba88..efdcaf3 100644 --- a/frontend/src/components/strategyEditor/IndicatorPaletteTab.tsx +++ b/frontend/src/components/strategyEditor/IndicatorPaletteTab.tsx @@ -12,14 +12,12 @@ import { type PaletteItemKind, } from '../../utils/strategyPaletteStorage'; +/** 팔레트 카드 기간 표시 — 항상 보조지표 설정(DEF) 기준 */ function periodLabel(item: PaletteItem, def: DefType): string { if (item.kind === 'composite') { const d = getCompositeDefaultPeriods(item.value, def); - const s = item.shortPeriod ?? d.short; - const l = item.longPeriod ?? d.long; - return `${s} / ${l}`; + return `${d.short} / ${d.long}`; } - if (item.period != null) return String(item.period); return getIndicatorPeriodLabel(item.value, def) || String(getDefaultIndicatorPeriod(item.value, def)); } diff --git a/frontend/src/components/strategyEditor/PaletteItemModal.tsx b/frontend/src/components/strategyEditor/PaletteItemModal.tsx index a6ce18c..2c188c7 100644 --- a/frontend/src/components/strategyEditor/PaletteItemModal.tsx +++ b/frontend/src/components/strategyEditor/PaletteItemModal.tsx @@ -39,7 +39,7 @@ export default function PaletteItemModal({ setLabel(initial.label); setDesc(initial.desc); if (kind === 'auxiliary') { - setPeriod(initial.period ?? getDefaultIndicatorPeriod(initial.value, def)); + setPeriod(getDefaultIndicatorPeriod(initial.value, def)); } else { const d = getCompositeDefaultPeriods(initial.value, def); setShortPeriod(initial.shortPeriod ?? d.short); diff --git a/frontend/src/hooks/useIndicatorSettings.ts b/frontend/src/hooks/useIndicatorSettings.ts index aa70a1a..0cece76 100644 --- a/frontend/src/hooks/useIndicatorSettings.ts +++ b/frontend/src/hooks/useIndicatorSettings.ts @@ -30,6 +30,7 @@ import { type IchimokuCloudColors, resolveIchimokuCloudColors, } from '../utils/ichimokuConfig'; +import type { IndicatorConfig } from '../types'; import { loadIndicatorSettings, saveIndicatorSettings, @@ -173,6 +174,49 @@ export function useIndicatorSettings(sessionKey = 0) { [] ); + /** + * 설정 화면 — 보조지표 기본값 일괄 저장 (파라미터 PUT + 시각 PATCH). + * 저장 완료 후 캐시 갱신 및 구독자(settingsRevision) 알림. + */ + const saveAllIndicatorDefaults = useCallback( + async (configs: Record) => { + const entries = Object.values(configs); + if (entries.length === 0) return; + + const allParams: AllSettings = { ...(_cache ?? {}) }; + for (const cfg of entries) { + allParams[cfg.type] = cfg.params as ParamMap; + } + + await saveAllIndicatorSettings(allParams); + _cache = { ...allParams }; + + const visualEntries = entries.map(cfg => { + const visual: IndicatorVisual = { + plots: cfg.plots, + hlines: cfg.hlines, + }; + if (cfg.cloudColors) visual.cloudColors = cfg.cloudColors; + return { type: cfg.type, visual }; + }); + + await Promise.all( + visualEntries.map(({ type, visual }) => + saveIndicatorVisualSettings(type, visual as { plots?: unknown[]; hlines?: unknown[]; cloudColors?: IchimokuCloudColors }), + ), + ); + + const nextVisual: VisualCache = { ...(_visualCache ?? {}) }; + for (const { type, visual } of visualEntries) { + nextVisual[type] = visual; + } + _visualCache = nextVisual; + + notifyIndicatorSettingsChanged(); + }, + [], + ); + // ── 시각 설정 (색상·선굵기·수평선) ────────────────────────────────────── /** @@ -250,5 +294,13 @@ export function useIndicatorSettings(sessionKey = 0) { [] ); - return { getParams, saveParams, saveAll, getVisualConfig, saveVisual, settingsRevision }; + return { + getParams, + saveParams, + saveAll, + saveAllIndicatorDefaults, + getVisualConfig, + saveVisual, + settingsRevision, + }; } diff --git a/frontend/src/utils/conditionPeriods.ts b/frontend/src/utils/conditionPeriods.ts index a4b4d08..4a9150e 100644 --- a/frontend/src/utils/conditionPeriods.ts +++ b/frontend/src/utils/conditionPeriods.ts @@ -69,27 +69,17 @@ function parseFieldPeriod(field?: string): number | null { return Number.isFinite(n) && n > 0 ? n : null; } -/** 조건 노드의 RSI 등 값(좌측) 기간 — 오버라이드 시 DSL, 아니면 보조지표 설정(DEF) */ +/** 조건 노드의 RSI 등 값(좌측) 기간 — true=전략 전용, false/미설정=보조지표 설정(DEF) 상속 */ export function isValuePeriodOverridden(cond: ConditionDSL): boolean { - if (cond.valuePeriodOverride === true) return true; - if (cond.valuePeriodOverride === false) return false; - if (cond.period != null && cond.period > 0) return true; - if (cond.leftPeriod != null && cond.leftPeriod > 0) return true; - if (isValuePeriodFieldStored(cond.indicatorType, cond.leftField)) return true; - return false; + return cond.valuePeriodOverride === true; } export function isRightPeriodOverridden(cond: ConditionDSL): boolean { - if (cond.rightPeriodOverride === true) return true; - if (cond.rightPeriodOverride === false) return false; - if (cond.rightPeriod != null && cond.rightPeriod > 0) return true; - return false; + return cond.rightPeriodOverride === true; } export function isThresholdOverridden(cond: ConditionDSL): boolean { - if (cond.thresholdOverride === true) return true; - if (cond.thresholdOverride === false) return false; - return false; + return cond.thresholdOverride === true; } /** 조건 노드의 RSI 등 값(좌측) 기간 */ diff --git a/frontend/src/utils/indicatorDefaultsFingerprint.ts b/frontend/src/utils/indicatorDefaultsFingerprint.ts new file mode 100644 index 0000000..3430458 --- /dev/null +++ b/frontend/src/utils/indicatorDefaultsFingerprint.ts @@ -0,0 +1,17 @@ +import type { IndicatorConfig } from '../types'; + +/** 설정 화면 — DB 기본값 변경 여부 비교용 */ +export function indicatorDefaultsFingerprint(c: IndicatorConfig): string { + return JSON.stringify({ + type: c.type, + params: c.params, + plots: c.plots, + plotVisibility: c.plotVisibility, + hlines: c.hlines, + hlinesBackground: c.hlinesBackground, + bandBackground: c.bandBackground, + cloudColors: c.cloudColors, + lastValueVisible: c.lastValueVisible, + timeframeVisibility: c.timeframeVisibility, + }); +} diff --git a/frontend/src/utils/strategyDefSync.ts b/frontend/src/utils/strategyDefSync.ts index 38fd618..694496f 100644 --- a/frontend/src/utils/strategyDefSync.ts +++ b/frontend/src/utils/strategyDefSync.ts @@ -39,20 +39,14 @@ function applyGlobalDefToCondition( if (cond.composite) { const { short, long } = getCompositeDefaultPeriods(cond.indicatorType, def as CompositePeriodDef); - if (!isValuePeriodOverridden(cond)) { + const inheritLeft = !isValuePeriodOverridden(cond); + const inheritRight = !isRightPeriodOverridden(cond); + if (inheritLeft || inheritRight) { next = syncCompositeFields({ ...next, composite: true, - leftPeriod: short, - valuePeriodOverride: false, - }); - } - if (!isRightPeriodOverridden(cond)) { - next = syncCompositeFields({ - ...next, - composite: true, - rightPeriod: long, - rightPeriodOverride: false, + ...(inheritLeft ? { leftPeriod: short, valuePeriodOverride: false as const } : null), + ...(inheritRight ? { rightPeriod: long, rightPeriodOverride: false as const } : null), }); } return next; diff --git a/frontend/src/utils/strategyEditorShared.tsx b/frontend/src/utils/strategyEditorShared.tsx index 8b1a66a..1825045 100644 --- a/frontend/src/utils/strategyEditorShared.tsx +++ b/frontend/src/utils/strategyEditorShared.tsx @@ -1086,13 +1086,10 @@ export function makeNodeOptionsFromPalette(data: { if (data.composite) { return { composite: true, - leftPeriod: data.shortPeriod, - rightPeriod: data.longPeriod, leftCandleType: data.leftCandleType, rightCandleType: data.rightCandleType, }; } - if (data.period != null) return { period: data.period }; return undefined; } @@ -1106,12 +1103,16 @@ export const makeNode = ( if (type === 'operator') return { id: genId(), type: value as LogicNodeType, children: [] }; if (options?.composite) { let condition = makeCompositeCondition(value, signalType, DEF); - if (options.leftPeriod != null || options.rightPeriod != null - || options.leftCandleType != null || options.rightCandleType != null) { + condition = { + ...condition, + valuePeriodOverride: false, + rightPeriodOverride: false, + thresholdOverride: false, + }; + if (options.leftCandleType != null || options.rightCandleType != null) { condition = syncCompositeFields({ ...condition, - leftPeriod: options.leftPeriod ?? condition.leftPeriod, - rightPeriod: options.rightPeriod ?? condition.rightPeriod, + composite: true, leftCandleType: options.leftCandleType ?? condition.leftCandleType, rightCandleType: options.rightCandleType ?? condition.rightCandleType, }); diff --git a/frontend/src/utils/strategyPaletteStorage.ts b/frontend/src/utils/strategyPaletteStorage.ts index 6982b47..48e90ce 100644 --- a/frontend/src/utils/strategyPaletteStorage.ts +++ b/frontend/src/utils/strategyPaletteStorage.ts @@ -116,7 +116,17 @@ export function loadPaletteItems(kind: PaletteItemKind): PaletteItem[] { try { const stored = parseStored(localStorage.getItem(key)); if (stored && stored.length > 0) { - return mergeMissingBuiltIns(stored.map(i => ({ ...i, kind })), kind, defaults); + return mergeMissingBuiltIns( + stored.map(i => ({ + ...i, + kind, + ...(i.builtIn + ? { period: undefined, shortPeriod: undefined, longPeriod: undefined } + : {}), + })), + kind, + defaults, + ); } } catch { /* ignore */ } return seedItems(kind, defaults);