보조지표 기준값 변경시 전략편집기 연동
This commit is contained in:
@@ -8701,6 +8701,30 @@ html.theme-blue {
|
|||||||
background: var(--bg2);
|
background: var(--bg2);
|
||||||
flex-shrink: 0;
|
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 {
|
.stg-content-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ function App() {
|
|||||||
saveParams: saveIndicatorParams,
|
saveParams: saveIndicatorParams,
|
||||||
getVisualConfig: getIndicatorVisual,
|
getVisualConfig: getIndicatorVisual,
|
||||||
saveVisual: saveIndicatorVisual,
|
saveVisual: saveIndicatorVisual,
|
||||||
|
saveAllIndicatorDefaults,
|
||||||
|
settingsRevision: indicatorSettingsRevision,
|
||||||
} = useIndicatorSettings(sessionKey);
|
} = useIndicatorSettings(sessionKey);
|
||||||
const getParams = getIndicatorParams;
|
const getParams = getIndicatorParams;
|
||||||
const saveParams = saveIndicatorParams;
|
const saveParams = saveIndicatorParams;
|
||||||
@@ -1704,6 +1706,8 @@ function App() {
|
|||||||
saveIndicatorParams={saveIndicatorParams}
|
saveIndicatorParams={saveIndicatorParams}
|
||||||
getIndicatorVisual={getIndicatorVisual}
|
getIndicatorVisual={getIndicatorVisual}
|
||||||
saveIndicatorVisual={saveIndicatorVisual}
|
saveIndicatorVisual={saveIndicatorVisual}
|
||||||
|
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
|
||||||
|
indicatorSettingsRevision={indicatorSettingsRevision}
|
||||||
chartIndicators={bulkSettingsIndicators}
|
chartIndicators={bulkSettingsIndicators}
|
||||||
onAddIndicatorToChart={handleAddIndicator}
|
onAddIndicatorToChart={handleAddIndicator}
|
||||||
onRemoveIndicatorFromChart={handleRemoveByType}
|
onRemoveIndicatorFromChart={handleRemoveByType}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|||||||
import type { IndicatorConfig } from '../types';
|
import type { IndicatorConfig } from '../types';
|
||||||
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
||||||
import { buildMainIndicatorConfig } from '../utils/indicatorMainConfig';
|
import { buildMainIndicatorConfig } from '../utils/indicatorMainConfig';
|
||||||
import { indicatorTypesOnChart } from '../utils/indicatorMainConfig';
|
import { indicatorDefaultsFingerprint } from '../utils/indicatorDefaultsFingerprint';
|
||||||
import IndicatorSettingsForm from './IndicatorSettingsForm';
|
import IndicatorSettingsForm from './IndicatorSettingsForm';
|
||||||
import { resetConfigToDefaults } from '../utils/indicatorSettingsEditor';
|
import { resetConfigToDefaults } from '../utils/indicatorSettingsEditor';
|
||||||
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
|
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
|
||||||
@@ -18,6 +18,13 @@ import IndicatorSettingsListSearch from './IndicatorSettingsListSearch';
|
|||||||
import IndicatorSettingsListRow from './IndicatorSettingsListRow';
|
import IndicatorSettingsListRow from './IndicatorSettingsListRow';
|
||||||
import { MAIN_INDICATOR_TYPES } from '../utils/indicatorMainTab';
|
import { MAIN_INDICATOR_TYPES } from '../utils/indicatorMainTab';
|
||||||
|
|
||||||
|
export interface IndicatorSaveUiState {
|
||||||
|
save: () => Promise<void>;
|
||||||
|
dirty: boolean;
|
||||||
|
saving: boolean;
|
||||||
|
saveMessage: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IndicatorMainDefaultsPanelProps {
|
export interface IndicatorMainDefaultsPanelProps {
|
||||||
chartIndicators: IndicatorConfig[];
|
chartIndicators: IndicatorConfig[];
|
||||||
onAddToChart: (type: string, config?: IndicatorConfig) => void;
|
onAddToChart: (type: string, config?: IndicatorConfig) => void;
|
||||||
@@ -35,32 +42,65 @@ export interface IndicatorMainDefaultsPanelProps {
|
|||||||
hlines?: HLineDef[],
|
hlines?: HLineDef[],
|
||||||
cloudColors?: IchimokuCloudColors,
|
cloudColors?: IchimokuCloudColors,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
/** DB에 보조지표 기본값 일괄 저장 */
|
||||||
|
saveAllDefaults?: (configs: Record<string, IndicatorConfig>) => Promise<void>;
|
||||||
function persistDefaults(
|
/** 상단 저장 버튼 상태 (SettingsPage 헤더 연동) */
|
||||||
updated: IndicatorConfig,
|
onSaveUiState?: (state: IndicatorSaveUiState | null) => void;
|
||||||
saveParams: IndicatorMainDefaultsPanelProps['saveParams'],
|
/** DB 로드 완료·저장 후 재로드 트리거 */
|
||||||
saveVisual: IndicatorMainDefaultsPanelProps['saveVisual'],
|
settingsRevision?: number;
|
||||||
) {
|
|
||||||
saveParams(updated.type, updated.params);
|
|
||||||
saveVisual(updated.type, updated.plots, updated.hlines, updated.cloudColors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL_TYPES = getSettingsIndicatorTypes();
|
const ALL_TYPES = getSettingsIndicatorTypes();
|
||||||
|
|
||||||
|
function buildDefaultsMap(
|
||||||
|
getParams: IndicatorMainDefaultsPanelProps['getParams'],
|
||||||
|
getVisualConfig: IndicatorMainDefaultsPanelProps['getVisualConfig'],
|
||||||
|
): Record<string, IndicatorConfig> {
|
||||||
|
const next: Record<string, IndicatorConfig> = {};
|
||||||
|
for (const type of ALL_TYPES) {
|
||||||
|
const cfg = buildMainIndicatorConfig(type, [], getParams, getVisualConfig);
|
||||||
|
if (cfg) next[type] = cfg;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fingerprintMap(map: Record<string, IndicatorConfig>): string {
|
||||||
|
return ALL_TYPES.map(t => map[t] ? indicatorDefaultsFingerprint(map[t]) : '').join('|');
|
||||||
|
}
|
||||||
|
|
||||||
const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
||||||
chartIndicators,
|
chartIndicators,
|
||||||
onAddToChart,
|
onAddToChart,
|
||||||
onRemoveFromChart,
|
onRemoveFromChart,
|
||||||
getParams,
|
getParams,
|
||||||
saveParams,
|
|
||||||
getVisualConfig,
|
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<Set<string>>(() => new Set());
|
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(() => new Set());
|
||||||
const [configs, setConfigs] = useState<Record<string, IndicatorConfig>>({});
|
const [configs, setConfigs] = useState<Record<string, IndicatorConfig>>({});
|
||||||
|
const [baselineFp, setBaselineFp] = useState('');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saveMessage, setSaveMessage] = useState<string | null>(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(
|
const filtered = useMemo(
|
||||||
() => filterSettingsIndicatorTypes(ALL_TYPES, search),
|
() => filterSettingsIndicatorTypes(ALL_TYPES, search),
|
||||||
@@ -71,15 +111,6 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
|||||||
[filtered],
|
[filtered],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const next: Record<string, IndicatorConfig> = {};
|
|
||||||
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) => {
|
const toggleExpand = useCallback((type: string) => {
|
||||||
setExpandedTypes(prev => {
|
setExpandedTypes(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -91,6 +122,7 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
|||||||
|
|
||||||
const handleConfigChange = useCallback((type: string, updated: IndicatorConfig) => {
|
const handleConfigChange = useCallback((type: string, updated: IndicatorConfig) => {
|
||||||
setConfigs(prev => ({ ...prev, [type]: updated }));
|
setConfigs(prev => ({ ...prev, [type]: updated }));
|
||||||
|
setSaveMessage(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleToggleChart = useCallback((type: string, enabled: boolean) => {
|
const handleToggleChart = useCallback((type: string, enabled: boolean) => {
|
||||||
@@ -105,22 +137,52 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
|||||||
const base = configs[type];
|
const base = configs[type];
|
||||||
if (!base) return;
|
if (!base) return;
|
||||||
const reset = resetConfigToDefaults(base);
|
const reset = resetConfigToDefaults(base);
|
||||||
persistDefaults(reset, saveParams, saveVisual);
|
|
||||||
handleConfigChange(type, reset);
|
handleConfigChange(type, reset);
|
||||||
}, [configs, saveParams, saveVisual, handleConfigChange]);
|
}, [configs, handleConfigChange]);
|
||||||
|
|
||||||
const handleResetAll = useCallback(() => {
|
const handleResetAll = useCallback(() => {
|
||||||
if (!window.confirm('목록에 있는 모든 보조지표 기본값을 초기화할까요?')) return;
|
if (!window.confirm('목록에 있는 모든 보조지표 기본값을 초기화할까요? (저장 버튼을 눌러야 DB에 반영됩니다)')) return;
|
||||||
const next: Record<string, IndicatorConfig> = {};
|
const next: Record<string, IndicatorConfig> = {};
|
||||||
for (const type of ALL_TYPES) {
|
for (const type of ALL_TYPES) {
|
||||||
const base = configs[type] ?? buildMainIndicatorConfig(type, [], getParams, getVisualConfig);
|
const base = configs[type] ?? buildMainIndicatorConfig(type, [], getParams, getVisualConfig);
|
||||||
if (!base) continue;
|
if (!base) continue;
|
||||||
const reset = resetConfigToDefaults(base);
|
next[type] = resetConfigToDefaults(base);
|
||||||
next[type] = reset;
|
|
||||||
persistDefaults(reset, saveParams, saveVisual);
|
|
||||||
}
|
}
|
||||||
setConfigs(next);
|
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 renderRows = (types: string[]) => types.map(type => {
|
||||||
const cfg = configs[type];
|
const cfg = configs[type];
|
||||||
@@ -135,10 +197,7 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
|||||||
variant="settings"
|
variant="settings"
|
||||||
onToggleExpand={() => toggleExpand(type)}
|
onToggleExpand={() => toggleExpand(type)}
|
||||||
onToggleEnabled={on => handleToggleChart(type, on)}
|
onToggleEnabled={on => handleToggleChart(type, on)}
|
||||||
onChange={updated => {
|
onChange={updated => handleConfigChange(type, updated)}
|
||||||
persistDefaults(updated, saveParams, saveVisual);
|
|
||||||
handleConfigChange(type, updated);
|
|
||||||
}}
|
|
||||||
onRowDefaults={() => handleRowDefaults(type)}
|
onRowDefaults={() => handleRowDefaults(type)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -149,7 +208,7 @@ const IndicatorMainDefaultsPanel: React.FC<IndicatorMainDefaultsPanelProps> = ({
|
|||||||
<div className="stg-ind-intro">
|
<div className="stg-ind-intro">
|
||||||
<p>
|
<p>
|
||||||
지표 추가 팝업 <strong>Main</strong> 탭 {MAIN_INDICATOR_TYPES.length}종을 상단에 두었고, 그 아래에 등록된 모든 보조지표가 있습니다.
|
지표 추가 팝업 <strong>Main</strong> 탭 {MAIN_INDICATOR_TYPES.length}종을 상단에 두었고, 그 아래에 등록된 모든 보조지표가 있습니다.
|
||||||
우측 스위치로 차트에 바로 추가·제거할 수 있으며, 파라미터·색상 변경은 DB 기본값에 자동 저장됩니다.
|
우측 스위치로 차트에 바로 추가·제거할 수 있습니다. 파라미터·색상을 변경한 뒤 <strong>상단 저장</strong> 버튼을 눌러 DB에 반영하세요.
|
||||||
</p>
|
</p>
|
||||||
<button type="button" className="stg-ind-reset-all" onClick={handleResetAll}>
|
<button type="button" className="stg-ind-reset-all" onClick={handleResetAll}>
|
||||||
전체 기본값 복원
|
전체 기본값 복원
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import {
|
|||||||
type LiveStrategySettingsDto,
|
type LiveStrategySettingsDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import { BacktestSettingsPanel } from './BacktestSettingsPanel';
|
import { BacktestSettingsPanel } from './BacktestSettingsPanel';
|
||||||
import IndicatorMainDefaultsPanel from './IndicatorMainDefaultsPanel';
|
import IndicatorMainDefaultsPanel, {
|
||||||
|
type IndicatorSaveUiState,
|
||||||
|
} from './IndicatorMainDefaultsPanel';
|
||||||
import type { IndicatorConfig } from '../types';
|
import type { IndicatorConfig } from '../types';
|
||||||
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
||||||
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
|
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
|
||||||
@@ -90,6 +92,10 @@ interface SettingsPageProps {
|
|||||||
hlines?: HLineDef[],
|
hlines?: HLineDef[],
|
||||||
cloudColors?: IchimokuCloudColors,
|
cloudColors?: IchimokuCloudColors,
|
||||||
) => void;
|
) => void;
|
||||||
|
/** 설정 화면 — 보조지표 기본값 일괄 DB 저장 */
|
||||||
|
saveAllIndicatorDefaults?: (configs: Record<string, IndicatorConfig>) => Promise<void>;
|
||||||
|
/** 보조지표 DB 캐시 갱신 revision */
|
||||||
|
indicatorSettingsRevision?: number;
|
||||||
/** 설정 화면에서 차트 on/off 연동 */
|
/** 설정 화면에서 차트 on/off 연동 */
|
||||||
chartIndicators?: IndicatorConfig[];
|
chartIndicators?: IndicatorConfig[];
|
||||||
onAddIndicatorToChart?: (type: string, config?: IndicatorConfig) => void;
|
onAddIndicatorToChart?: (type: string, config?: IndicatorConfig) => void;
|
||||||
@@ -1395,6 +1401,8 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
saveIndicatorParams,
|
saveIndicatorParams,
|
||||||
getIndicatorVisual,
|
getIndicatorVisual,
|
||||||
saveIndicatorVisual,
|
saveIndicatorVisual,
|
||||||
|
saveAllIndicatorDefaults,
|
||||||
|
indicatorSettingsRevision = 0,
|
||||||
chartIndicators = [],
|
chartIndicators = [],
|
||||||
onAddIndicatorToChart,
|
onAddIndicatorToChart,
|
||||||
onRemoveIndicatorFromChart,
|
onRemoveIndicatorFromChart,
|
||||||
@@ -1455,6 +1463,7 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const categories = filterCategories(menuPermissions);
|
const categories = filterCategories(menuPermissions);
|
||||||
const [active, setActive] = useState<CategoryId>('general');
|
const [active, setActive] = useState<CategoryId>('general');
|
||||||
|
const [indicatorSaveUi, setIndicatorSaveUi] = useState<IndicatorSaveUiState | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (categories.length === 0) return;
|
if (categories.length === 0) return;
|
||||||
@@ -1463,6 +1472,10 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
}
|
}
|
||||||
}, [categories, active]);
|
}, [categories, active]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (active !== 'indicators') setIndicatorSaveUi(null);
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
const activeCat = categories.find(c => c.id === active) ?? categories[0];
|
const activeCat = categories.find(c => c.id === active) ?? categories[0];
|
||||||
|
|
||||||
if (!activeCat) {
|
if (!activeCat) {
|
||||||
@@ -1494,6 +1507,7 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
case 'indicators':
|
case 'indicators':
|
||||||
if (
|
if (
|
||||||
!getIndicatorParams || !saveIndicatorParams || !getIndicatorVisual || !saveIndicatorVisual
|
!getIndicatorParams || !saveIndicatorParams || !getIndicatorVisual || !saveIndicatorVisual
|
||||||
|
|| !saveAllIndicatorDefaults
|
||||||
|| !onAddIndicatorToChart || !onRemoveIndicatorFromChart
|
|| !onAddIndicatorToChart || !onRemoveIndicatorFromChart
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -1509,6 +1523,9 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
saveParams={saveIndicatorParams}
|
saveParams={saveIndicatorParams}
|
||||||
getVisualConfig={getIndicatorVisual}
|
getVisualConfig={getIndicatorVisual}
|
||||||
saveVisual={saveIndicatorVisual}
|
saveVisual={saveIndicatorVisual}
|
||||||
|
saveAllDefaults={saveAllIndicatorDefaults}
|
||||||
|
onSaveUiState={setIndicatorSaveUi}
|
||||||
|
settingsRevision={indicatorSettingsRevision}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'backtest': return (
|
case 'backtest': return (
|
||||||
@@ -1631,10 +1648,31 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="stg-content-header">
|
<div className="stg-content-header">
|
||||||
<div className="stg-content-icon">{activeCat.icon}</div>
|
<div className="stg-content-icon">{activeCat.icon}</div>
|
||||||
<div>
|
<div className="stg-content-header-text">
|
||||||
<h2 className="stg-content-title">{activeCat.label}</h2>
|
<h2 className="stg-content-title">{activeCat.label}</h2>
|
||||||
<p className="stg-content-desc">{activeCat.desc}</p>
|
<p className="stg-content-desc">{activeCat.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{active === 'indicators' && indicatorSaveUi && (
|
||||||
|
<div className="stg-content-header-actions">
|
||||||
|
{indicatorSaveUi.saveMessage && (
|
||||||
|
<span
|
||||||
|
className={`stg-ind-save-msg stg-ind-save-msg--inline${
|
||||||
|
indicatorSaveUi.saveMessage.includes('실패') ? ' stg-ind-save-msg--err' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{indicatorSaveUi.saveMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="stg-btn-save"
|
||||||
|
disabled={!indicatorSaveUi.dirty || indicatorSaveUi.saving}
|
||||||
|
onClick={() => void indicatorSaveUi.save()}
|
||||||
|
>
|
||||||
|
{indicatorSaveUi.saving ? '저장 중…' : '저장'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 설정 패널 */}
|
{/* 설정 패널 */}
|
||||||
@@ -1649,11 +1687,6 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
<button className="stg-btn-save">저장</button>
|
<button className="stg-btn-save">저장</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{active === 'indicators' && (
|
|
||||||
<div className="stg-footer stg-footer--hint">
|
|
||||||
<span className="stg-footer-hint">보조지표 기본값은 항목을 수정할 때마다 자동으로 저장됩니다.</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -254,7 +254,29 @@ export default function StrategyEditorPage({ theme }: Props) {
|
|||||||
|
|
||||||
const orphanTotal = buyOrphans.length + sellOrphans.length;
|
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(() => {
|
useEffect(() => {
|
||||||
if (settingsRevision <= settingsSyncRef.current) return;
|
if (settingsRevision <= settingsSyncRef.current) return;
|
||||||
settingsSyncRef.current = settingsRevision;
|
settingsSyncRef.current = settingsRevision;
|
||||||
@@ -282,29 +304,8 @@ export default function StrategyEditorPage({ theme }: Props) {
|
|||||||
...prev,
|
...prev,
|
||||||
extraRoots: syncExtraRoots(prev.extraRoots ?? {}, 'sell'),
|
extraRoots: syncExtraRoots(prev.extraRoots ?? {}, 'sell'),
|
||||||
}));
|
}));
|
||||||
}, [settingsRevision, DEF]);
|
bumpLayoutSeed(layoutStrategyKey, signalTab);
|
||||||
|
}, [settingsRevision, DEF, layoutStrategyKey, signalTab, bumpLayoutSeed]);
|
||||||
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}`);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const resetFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => {
|
const resetFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => {
|
||||||
const emptyBuy = emptySignalFlowLayout();
|
const emptyBuy = emptySignalFlowLayout();
|
||||||
@@ -738,12 +739,7 @@ export default function StrategyEditorPage({ theme }: Props) {
|
|||||||
|
|
||||||
const applyPaletteItem = useCallback((item: PaletteItem) => {
|
const applyPaletteItem = useCallback((item: PaletteItem) => {
|
||||||
const composite = item.kind === 'composite';
|
const composite = item.kind === 'composite';
|
||||||
const newNode = makeNode('indicator', item.value, signalTab, DEF, {
|
const newNode = makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined);
|
||||||
composite,
|
|
||||||
period: item.period,
|
|
||||||
leftPeriod: item.shortPeriod,
|
|
||||||
rightPeriod: item.longPeriod,
|
|
||||||
});
|
|
||||||
const root = currentRoot;
|
const root = currentRoot;
|
||||||
if (!root) setCurrentRoot(newNode);
|
if (!root) setCurrentRoot(newNode);
|
||||||
else setCurrentRoot(mergeAtRoot(root, newNode, false));
|
else setCurrentRoot(mergeAtRoot(root, newNode, false));
|
||||||
|
|||||||
@@ -12,14 +12,12 @@ import {
|
|||||||
type PaletteItemKind,
|
type PaletteItemKind,
|
||||||
} from '../../utils/strategyPaletteStorage';
|
} from '../../utils/strategyPaletteStorage';
|
||||||
|
|
||||||
|
/** 팔레트 카드 기간 표시 — 항상 보조지표 설정(DEF) 기준 */
|
||||||
function periodLabel(item: PaletteItem, def: DefType): string {
|
function periodLabel(item: PaletteItem, def: DefType): string {
|
||||||
if (item.kind === 'composite') {
|
if (item.kind === 'composite') {
|
||||||
const d = getCompositeDefaultPeriods(item.value, def);
|
const d = getCompositeDefaultPeriods(item.value, def);
|
||||||
const s = item.shortPeriod ?? d.short;
|
return `${d.short} / ${d.long}`;
|
||||||
const l = item.longPeriod ?? d.long;
|
|
||||||
return `${s} / ${l}`;
|
|
||||||
}
|
}
|
||||||
if (item.period != null) return String(item.period);
|
|
||||||
return getIndicatorPeriodLabel(item.value, def) || String(getDefaultIndicatorPeriod(item.value, def));
|
return getIndicatorPeriodLabel(item.value, def) || String(getDefaultIndicatorPeriod(item.value, def));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default function PaletteItemModal({
|
|||||||
setLabel(initial.label);
|
setLabel(initial.label);
|
||||||
setDesc(initial.desc);
|
setDesc(initial.desc);
|
||||||
if (kind === 'auxiliary') {
|
if (kind === 'auxiliary') {
|
||||||
setPeriod(initial.period ?? getDefaultIndicatorPeriod(initial.value, def));
|
setPeriod(getDefaultIndicatorPeriod(initial.value, def));
|
||||||
} else {
|
} else {
|
||||||
const d = getCompositeDefaultPeriods(initial.value, def);
|
const d = getCompositeDefaultPeriods(initial.value, def);
|
||||||
setShortPeriod(initial.shortPeriod ?? d.short);
|
setShortPeriod(initial.shortPeriod ?? d.short);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
type IchimokuCloudColors,
|
type IchimokuCloudColors,
|
||||||
resolveIchimokuCloudColors,
|
resolveIchimokuCloudColors,
|
||||||
} from '../utils/ichimokuConfig';
|
} from '../utils/ichimokuConfig';
|
||||||
|
import type { IndicatorConfig } from '../types';
|
||||||
import {
|
import {
|
||||||
loadIndicatorSettings,
|
loadIndicatorSettings,
|
||||||
saveIndicatorSettings,
|
saveIndicatorSettings,
|
||||||
@@ -173,6 +174,49 @@ export function useIndicatorSettings(sessionKey = 0) {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 설정 화면 — 보조지표 기본값 일괄 저장 (파라미터 PUT + 시각 PATCH).
|
||||||
|
* 저장 완료 후 캐시 갱신 및 구독자(settingsRevision) 알림.
|
||||||
|
*/
|
||||||
|
const saveAllIndicatorDefaults = useCallback(
|
||||||
|
async (configs: Record<string, IndicatorConfig>) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,27 +69,17 @@ function parseFieldPeriod(field?: string): number | null {
|
|||||||
return Number.isFinite(n) && n > 0 ? n : null;
|
return Number.isFinite(n) && n > 0 ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 조건 노드의 RSI 등 값(좌측) 기간 — 오버라이드 시 DSL, 아니면 보조지표 설정(DEF) */
|
/** 조건 노드의 RSI 등 값(좌측) 기간 — true=전략 전용, false/미설정=보조지표 설정(DEF) 상속 */
|
||||||
export function isValuePeriodOverridden(cond: ConditionDSL): boolean {
|
export function isValuePeriodOverridden(cond: ConditionDSL): boolean {
|
||||||
if (cond.valuePeriodOverride === true) return true;
|
return cond.valuePeriodOverride === 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRightPeriodOverridden(cond: ConditionDSL): boolean {
|
export function isRightPeriodOverridden(cond: ConditionDSL): boolean {
|
||||||
if (cond.rightPeriodOverride === true) return true;
|
return cond.rightPeriodOverride === true;
|
||||||
if (cond.rightPeriodOverride === false) return false;
|
|
||||||
if (cond.rightPeriod != null && cond.rightPeriod > 0) return true;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isThresholdOverridden(cond: ConditionDSL): boolean {
|
export function isThresholdOverridden(cond: ConditionDSL): boolean {
|
||||||
if (cond.thresholdOverride === true) return true;
|
return cond.thresholdOverride === true;
|
||||||
if (cond.thresholdOverride === false) return false;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 조건 노드의 RSI 등 값(좌측) 기간 */
|
/** 조건 노드의 RSI 등 값(좌측) 기간 */
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -39,20 +39,14 @@ function applyGlobalDefToCondition(
|
|||||||
|
|
||||||
if (cond.composite) {
|
if (cond.composite) {
|
||||||
const { short, long } = getCompositeDefaultPeriods(cond.indicatorType, def as CompositePeriodDef);
|
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 = syncCompositeFields({
|
||||||
...next,
|
...next,
|
||||||
composite: true,
|
composite: true,
|
||||||
leftPeriod: short,
|
...(inheritLeft ? { leftPeriod: short, valuePeriodOverride: false as const } : null),
|
||||||
valuePeriodOverride: false,
|
...(inheritRight ? { rightPeriod: long, rightPeriodOverride: false as const } : null),
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!isRightPeriodOverridden(cond)) {
|
|
||||||
next = syncCompositeFields({
|
|
||||||
...next,
|
|
||||||
composite: true,
|
|
||||||
rightPeriod: long,
|
|
||||||
rightPeriodOverride: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
|
|||||||
@@ -1086,13 +1086,10 @@ export function makeNodeOptionsFromPalette(data: {
|
|||||||
if (data.composite) {
|
if (data.composite) {
|
||||||
return {
|
return {
|
||||||
composite: true,
|
composite: true,
|
||||||
leftPeriod: data.shortPeriod,
|
|
||||||
rightPeriod: data.longPeriod,
|
|
||||||
leftCandleType: data.leftCandleType,
|
leftCandleType: data.leftCandleType,
|
||||||
rightCandleType: data.rightCandleType,
|
rightCandleType: data.rightCandleType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (data.period != null) return { period: data.period };
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1106,12 +1103,16 @@ export const makeNode = (
|
|||||||
if (type === 'operator') return { id: genId(), type: value as LogicNodeType, children: [] };
|
if (type === 'operator') return { id: genId(), type: value as LogicNodeType, children: [] };
|
||||||
if (options?.composite) {
|
if (options?.composite) {
|
||||||
let condition = makeCompositeCondition(value, signalType, DEF);
|
let condition = makeCompositeCondition(value, signalType, DEF);
|
||||||
if (options.leftPeriod != null || options.rightPeriod != null
|
condition = {
|
||||||
|| options.leftCandleType != null || options.rightCandleType != null) {
|
...condition,
|
||||||
|
valuePeriodOverride: false,
|
||||||
|
rightPeriodOverride: false,
|
||||||
|
thresholdOverride: false,
|
||||||
|
};
|
||||||
|
if (options.leftCandleType != null || options.rightCandleType != null) {
|
||||||
condition = syncCompositeFields({
|
condition = syncCompositeFields({
|
||||||
...condition,
|
...condition,
|
||||||
leftPeriod: options.leftPeriod ?? condition.leftPeriod,
|
composite: true,
|
||||||
rightPeriod: options.rightPeriod ?? condition.rightPeriod,
|
|
||||||
leftCandleType: options.leftCandleType ?? condition.leftCandleType,
|
leftCandleType: options.leftCandleType ?? condition.leftCandleType,
|
||||||
rightCandleType: options.rightCandleType ?? condition.rightCandleType,
|
rightCandleType: options.rightCandleType ?? condition.rightCandleType,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,7 +116,17 @@ export function loadPaletteItems(kind: PaletteItemKind): PaletteItem[] {
|
|||||||
try {
|
try {
|
||||||
const stored = parseStored(localStorage.getItem(key));
|
const stored = parseStored(localStorage.getItem(key));
|
||||||
if (stored && stored.length > 0) {
|
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 */ }
|
} catch { /* ignore */ }
|
||||||
return seedItems(kind, defaults);
|
return seedItems(kind, defaults);
|
||||||
|
|||||||
Reference in New Issue
Block a user