전략평가 차트설정탭 추가수정
This commit is contained in:
@@ -50,6 +50,8 @@ export interface IndicatorSettingsFormProps {
|
||||
chartMarket?: string;
|
||||
/** embedded: 일괄 설정 카드 안 */
|
||||
variant?: 'modal' | 'embedded';
|
||||
/** embedded 차트설정 등 — 출력·표시(가격축 라벨·타임프레임) 숨김 */
|
||||
hideOutputFooter?: boolean;
|
||||
/**
|
||||
* modal: 편집 중 부모 setState 없이 ref만 갱신 (포커스 유지).
|
||||
* embedded: 변경 즉시 onChange 호출.
|
||||
@@ -85,6 +87,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
||||
variant = 'modal',
|
||||
deferParentSync = variant === 'modal',
|
||||
inputsOnly = false,
|
||||
hideOutputFooter = false,
|
||||
chartHidden = false,
|
||||
onChartHiddenChange,
|
||||
onChange,
|
||||
@@ -289,7 +292,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!inputsOnly && <SettingsOutputFooter {...footerProps} />}
|
||||
{!inputsOnly && !hideOutputFooter && <SettingsOutputFooter {...footerProps} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -323,7 +326,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
||||
/>
|
||||
)}
|
||||
inputsOnly={inputsOnly}
|
||||
showOutputFooter={!inputsOnly}
|
||||
showOutputFooter={!inputsOnly && !hideOutputFooter}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -351,7 +354,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
||||
chartHidden={chartHidden}
|
||||
onChartHidden={onChartHiddenChange}
|
||||
inputsOnly={inputsOnly}
|
||||
showOutputFooter={!inputsOnly && !isBollinger}
|
||||
showOutputFooter={!inputsOnly && !hideOutputFooter && !isBollinger}
|
||||
symbolSection={isBollinger ? (
|
||||
<BollingerSymbolSection
|
||||
symbolMode={String(params.symbolMode ?? 'main')}
|
||||
|
||||
@@ -22,6 +22,10 @@ export interface IndicatorSettingsListRowProps {
|
||||
onDragOver?: (e: React.DragEvent) => void;
|
||||
onDragLeave?: () => void;
|
||||
onDrop?: (e: React.DragEvent) => void;
|
||||
/** 차트 on/off 토글 숨김 (전략 평가 차트설정 등) */
|
||||
hideChartToggle?: boolean;
|
||||
/** 출력·표시(가격축 라벨·타임프레임) 숨김 */
|
||||
hideOutputFooter?: boolean;
|
||||
}
|
||||
|
||||
const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
@@ -41,6 +45,8 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
hideChartToggle = false,
|
||||
hideOutputFooter = false,
|
||||
}) => {
|
||||
const labels = getIndicatorListLabels(type);
|
||||
const def = getIndicatorDef(type);
|
||||
@@ -88,24 +94,28 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
<div className={badgesClass}>
|
||||
{def?.overlay && <span className="ind-panel-badge">오버레이</span>}
|
||||
{def?.returnsMarkers && <span className="ind-panel-badge">마커</span>}
|
||||
{enabled ? (
|
||||
<span className={onBadgeClass}>차트 적용</span>
|
||||
) : (
|
||||
<span className={offBadgeClass}>미적용</span>
|
||||
{!hideChartToggle && (
|
||||
enabled ? (
|
||||
<span className={onBadgeClass}>차트 적용</span>
|
||||
) : (
|
||||
<span className={offBadgeClass}>미적용</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<label
|
||||
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
||||
title={enabled ? '차트에서 제거' : '차트에 추가'}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={e => onToggleEnabled(e.target.checked)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
{!hideChartToggle && (
|
||||
<label
|
||||
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
||||
title={enabled ? '차트에서 제거' : '차트에 추가'}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={e => onToggleEnabled(e.target.checked)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
)}
|
||||
{variant === 'settings' && onRowDefaults && (
|
||||
<button type="button" className="stg-ind-card-defaults" onClick={onRowDefaults}>
|
||||
기본값
|
||||
@@ -128,6 +138,7 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||
config={config}
|
||||
chartMarket={chartMarket}
|
||||
variant="embedded"
|
||||
hideOutputFooter={hideOutputFooter}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,7 @@ import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisRep
|
||||
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
||||
import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup';
|
||||
import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
|
||||
import StrategyEvaluationChartSettingsTab from './strategyEvaluation/StrategyEvaluationChartSettingsTab';
|
||||
import {
|
||||
buildStrategyEvaluationAiVerifyContext,
|
||||
requestStrategyEvaluationAiVerify,
|
||||
@@ -65,7 +66,7 @@ const LEFT_DEFAULT = 380;
|
||||
const RIGHT_DEFAULT = 440;
|
||||
|
||||
type LeftTab = 'strategy' | 'market' | 'settings';
|
||||
type RightTab = 'signal' | 'ai';
|
||||
type RightTab = 'signal' | 'chart' | 'ai';
|
||||
|
||||
function readMinWidth(key: string, fallback: number): number {
|
||||
return Math.max(readStoredSize(key, fallback), fallback);
|
||||
@@ -76,7 +77,13 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
const { getParams: baseGetParams, getVisualConfig } = useIndicatorSettings();
|
||||
const {
|
||||
getParams: baseGetParams,
|
||||
getVisualConfig,
|
||||
saveParams,
|
||||
saveVisual,
|
||||
settingsRevision,
|
||||
} = useIndicatorSettings();
|
||||
const [leftTab, setLeftTab] = useState<LeftTab>('strategy');
|
||||
const [marketSearchOpen, setMarketSearchOpen] = useState(false);
|
||||
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
|
||||
@@ -413,6 +420,16 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
setParamsRevision(v => v + 1);
|
||||
}, []);
|
||||
|
||||
const handleChartSettingsPersisted = useCallback((type: string) => {
|
||||
setAppliedIndicatorParams(prev => {
|
||||
if (!prev || !(type in prev)) return prev;
|
||||
const next = { ...prev };
|
||||
delete next[type];
|
||||
return Object.keys(next).length ? next : null;
|
||||
});
|
||||
// 차트설정은 DB 즉시 저장 + TradingChart indicators prop 갱신으로 부분 반영 — 전체 remount·캔들 재조회 생략
|
||||
}, []);
|
||||
|
||||
const strategyLabel = selectedStrategy?.name
|
||||
? repairUtf8Mojibake(selectedStrategy.name)
|
||||
: '전략 미선택';
|
||||
@@ -783,7 +800,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
side="right"
|
||||
open={rightOpen}
|
||||
onToggle={toggleRightPanel}
|
||||
label="시그널 패널"
|
||||
label={rightTab === 'chart' ? '차트설정 패널' : '시그널 패널'}
|
||||
/>
|
||||
{rightOpen && (
|
||||
<div
|
||||
@@ -807,6 +824,17 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
>
|
||||
시그널
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="seval-right-tab-chart"
|
||||
aria-selected={rightTab === 'chart'}
|
||||
aria-controls="seval-right-panel-chart"
|
||||
className={`btd-exec-tab seval-right-tab${rightTab === 'chart' ? ' btd-exec-tab--on' : ''}`}
|
||||
onClick={() => setRightTab('chart')}
|
||||
>
|
||||
차트설정
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
@@ -877,6 +905,26 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rightTab === 'chart' && (
|
||||
<div
|
||||
id="seval-right-panel-chart"
|
||||
role="tabpanel"
|
||||
aria-labelledby="seval-right-tab-chart"
|
||||
className="seval-right-tab-panel seval-right-tab-panel--chart"
|
||||
>
|
||||
<StrategyEvaluationChartSettingsTab
|
||||
strategy={selectedStrategy}
|
||||
market={market}
|
||||
getParams={baseGetParams}
|
||||
getVisualConfig={getVisualConfig}
|
||||
saveParams={saveParams}
|
||||
saveVisual={saveVisual}
|
||||
settingsRevision={settingsRevision}
|
||||
onPersisted={handleChartSettingsPersisted}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rightTab === 'ai' && (
|
||||
<div
|
||||
id="seval-right-panel-ai"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 지표 가격축 라벨·설명 — 캔들 오버레이 / 보조지표 pane 전역 on·off (설정 화면과 동일)
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
candleAreaEnabled: boolean;
|
||||
indicatorAreaEnabled: boolean;
|
||||
onCandleAreaChange: (enabled: boolean) => void;
|
||||
onIndicatorAreaChange: (enabled: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ChartPriceAxisLabelSettings: React.FC<Props> = ({
|
||||
candleAreaEnabled,
|
||||
indicatorAreaEnabled,
|
||||
onCandleAreaChange,
|
||||
onIndicatorAreaChange,
|
||||
className = '',
|
||||
}) => (
|
||||
<section className={`seval-price-label-settings${className ? ` ${className}` : ''}`}>
|
||||
<h3 className="seval-price-label-settings-title">지표 가격축 라벨·설명</h3>
|
||||
<div className="seval-price-label-settings-body">
|
||||
<div className="seval-price-label-subhead">
|
||||
캔들 차트 영역
|
||||
<span className="seval-price-label-subdesc">
|
||||
캔들·거래량 pane 위에 겹쳐 표시되는 지표(이동평균·일목 등)
|
||||
</span>
|
||||
</div>
|
||||
<div className="seval-price-label-row">
|
||||
<div className="seval-price-label-row-text">
|
||||
<span className="seval-price-label-row-title">가격축 라벨·설명</span>
|
||||
<span className="seval-price-label-row-desc">
|
||||
켜면 오버레이 지표 선마다 우측 가격축에 금액과 설명(전환선, 기준선 등)이 표시됩니다.
|
||||
</span>
|
||||
</div>
|
||||
<label className="ism-toggle seval-price-label-toggle" title="캔들 영역 가격축 라벨·설명">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={candleAreaEnabled}
|
||||
onChange={e => onCandleAreaChange(e.target.checked)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="seval-price-label-subhead seval-price-label-subhead--spaced">
|
||||
보조지표 영역
|
||||
<span className="seval-price-label-subdesc">
|
||||
캔들 아래 별도 pane에 표시되는 RSI·MACD 등
|
||||
</span>
|
||||
</div>
|
||||
<div className="seval-price-label-row">
|
||||
<div className="seval-price-label-row-text">
|
||||
<span className="seval-price-label-row-title">가격축 라벨·설명</span>
|
||||
<span className="seval-price-label-row-desc">
|
||||
켜면 하단 보조지표 pane의 선마다 우측 가격축에 금액과 설명이 표시됩니다. 끄면 선만 표시됩니다.
|
||||
</span>
|
||||
</div>
|
||||
<label className="ism-toggle seval-price-label-toggle" title="보조지표 영역 가격축 라벨·설명">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={indicatorAreaEnabled}
|
||||
onChange={e => onIndicatorAreaChange(e.target.checked)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
export default ChartPriceAxisLabelSettings;
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
} from '../../utils/chartOverlayVisibility';
|
||||
import { buildStrategyEvaluationChartIndicators } from '../../utils/strategyToChartIndicators';
|
||||
import { buildEvalParamsFromStrategy } from '../../utils/strategyEvaluationParams';
|
||||
import { indicatorDefaultsFingerprint } from '../../utils/indicatorDefaultsFingerprint';
|
||||
import { repairUtf8Mojibake } from '../../utils/textEncoding';
|
||||
import StrategyEvaluationBarSelector, {
|
||||
resolveBarIndexAtChartClick,
|
||||
@@ -150,6 +149,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
const getParams = getParamsOverride ?? baseGetParams;
|
||||
const getVisualConfig = getVisualConfigOverride ?? baseGetVisual;
|
||||
const { defaults: appDefaults } = useAppSettings();
|
||||
const candlePriceLabels = appDefaults.chartCandleAreaPriceLabels !== false;
|
||||
const indicatorPriceLabels = appDefaults.chartSeriesPriceLabels !== false;
|
||||
const [bars, setBars] = useState<OHLCVBar[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -297,9 +298,9 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
[strategy, getParams],
|
||||
);
|
||||
|
||||
const indicatorRenderKey = useMemo(
|
||||
() => indicators.map(i => indicatorDefaultsFingerprint(i)).join('|'),
|
||||
[indicators],
|
||||
const indicatorParamsKey = useMemo(
|
||||
() => JSON.stringify(indicatorParams ?? {}),
|
||||
[indicatorParams],
|
||||
);
|
||||
|
||||
const resolveInitialDisplayCount = useCallback(
|
||||
@@ -382,7 +383,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
}
|
||||
void runSignalScanForBars(bars);
|
||||
return () => { ++backtestGenRef.current; };
|
||||
}, [strategy?.id, strategy?.buyCondition, market, timeframe, paramsRevision, bars, loading, runSignalScanForBars]);
|
||||
}, [strategy?.id, strategy?.buyCondition, market, timeframe, paramsRevision, indicatorParamsKey, bars, loading, runSignalScanForBars]);
|
||||
|
||||
useEffect(() => {
|
||||
applyMarkers();
|
||||
@@ -694,7 +695,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
{!loading && !error && bars.length > 0 && (
|
||||
<>
|
||||
<TradingChart
|
||||
key={`${market}-${timeframe}-${strategy?.id ?? 0}-${paramsRevision}-${settingsRevision}-${indicatorRenderKey}`}
|
||||
key={`${market}-${timeframe}-${strategy?.id ?? 0}`}
|
||||
bars={bars}
|
||||
barsMarket={market}
|
||||
market={market}
|
||||
@@ -720,6 +721,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
crosshairInfoVisible={false}
|
||||
showHoverToolbar={false}
|
||||
showChartRightToolbar={false}
|
||||
candleAreaPriceLabelsEnabled={candlePriceLabels}
|
||||
indicatorAreaPriceLabelsEnabled={indicatorPriceLabels}
|
||||
showCandlePaneControls={false}
|
||||
showCandleOverlayControls
|
||||
candleOverlayToggles={candleOverlayToggles}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 전략 평가 — 우측 패널 차트설정 탭
|
||||
* 이동평균·볼린저·일목 + 전략 사용 지표 — 설정 화면과 동일 UI, 변경 즉시 DB·차트 반영
|
||||
*/
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { IndicatorConfig, HlinesBackground } from '../../types';
|
||||
import type { PlotDef, HLineDef } from '../../utils/indicatorRegistry';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import { buildMainIndicatorConfig } from '../../utils/indicatorMainConfig';
|
||||
import { collectStrategyRegistryTypes } from '../../utils/strategyToChartIndicators';
|
||||
import {
|
||||
filterSettingsIndicatorTypes,
|
||||
partitionFilteredTypes,
|
||||
} from '../../utils/indicatorSettingsList';
|
||||
import IndicatorSettingsListSearch from '../IndicatorSettingsListSearch';
|
||||
import IndicatorSettingsListRow from '../IndicatorSettingsListRow';
|
||||
import {
|
||||
persistIndicatorConfigToDb,
|
||||
resetConfigToDefaults,
|
||||
} from '../../utils/indicatorSettingsEditor';
|
||||
import type { IchimokuCloudColors } from '../../utils/ichimokuConfig';
|
||||
import ChartPriceAxisLabelSettings from './ChartPriceAxisLabelSettings';
|
||||
import { useAppSettings, resolveAppDefaults } from '../../hooks/useAppSettings';
|
||||
|
||||
const EVAL_CHART_OVERLAY_TYPES = ['SMA', 'BollingerBands', 'IchimokuCloud'] as const;
|
||||
const EVAL_CHART_OVERLAY_SET = new Set<string>(EVAL_CHART_OVERLAY_TYPES);
|
||||
|
||||
function buildEvalChartSettingsTypes(strategy: StrategyDto | null): string[] {
|
||||
const strategyTypes = strategy ? collectStrategyRegistryTypes(strategy) : [];
|
||||
const ordered: string[] = [...EVAL_CHART_OVERLAY_TYPES];
|
||||
for (const type of strategyTypes) {
|
||||
if (!EVAL_CHART_OVERLAY_SET.has(type)) ordered.push(type);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
strategy: StrategyDto | null;
|
||||
market: string;
|
||||
getParams: (
|
||||
type: string,
|
||||
defaults?: Record<string, number | string | boolean>,
|
||||
) => Record<string, number | string | boolean>;
|
||||
getVisualConfig: (
|
||||
type: string,
|
||||
defaultPlots?: PlotDef[],
|
||||
defaultHlines?: HLineDef[],
|
||||
) => {
|
||||
plots: PlotDef[];
|
||||
hlines: HLineDef[];
|
||||
cloudColors?: IchimokuCloudColors;
|
||||
bandBackground?: HlinesBackground;
|
||||
};
|
||||
saveParams: (type: string, params: Record<string, number | string | boolean>) => void;
|
||||
saveVisual: (
|
||||
type: string,
|
||||
plots?: PlotDef[],
|
||||
hlines?: HLineDef[],
|
||||
cloudColors?: IchimokuCloudColors,
|
||||
bandBackground?: HlinesBackground,
|
||||
plotVisibility?: Record<string, boolean>,
|
||||
) => void;
|
||||
settingsRevision?: number;
|
||||
/** DB 저장 후 세션 오버라이드 제거·차트 갱신 */
|
||||
onPersisted?: (type: string) => void;
|
||||
}
|
||||
|
||||
const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
||||
strategy,
|
||||
market,
|
||||
getParams,
|
||||
getVisualConfig,
|
||||
saveParams,
|
||||
saveVisual,
|
||||
settingsRevision = 0,
|
||||
onPersisted,
|
||||
}) => {
|
||||
const indicatorTypes = useMemo(
|
||||
() => buildEvalChartSettingsTypes(strategy),
|
||||
[strategy],
|
||||
);
|
||||
|
||||
const [configs, setConfigs] = useState<Record<string, IndicatorConfig>>({});
|
||||
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(() => new Set());
|
||||
const [search, setSearch] = useState('');
|
||||
const saveTimersRef = useRef<Record<string, number>>({});
|
||||
const { settings, save: saveAppSettings } = useAppSettings();
|
||||
const appDefaults = useMemo(() => resolveAppDefaults(settings), [settings]);
|
||||
const candlePriceLabels = appDefaults.chartCandleAreaPriceLabels !== false;
|
||||
const indicatorPriceLabels = appDefaults.chartSeriesPriceLabels !== false;
|
||||
|
||||
const reloadFromDb = useCallback(() => {
|
||||
const next: Record<string, IndicatorConfig> = {};
|
||||
for (const type of indicatorTypes) {
|
||||
const cfg = buildMainIndicatorConfig(type, [], getParams, getVisualConfig);
|
||||
if (cfg) next[type] = cfg;
|
||||
}
|
||||
setConfigs(next);
|
||||
}, [indicatorTypes, getParams, getVisualConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadFromDb();
|
||||
}, [reloadFromDb, settingsRevision]);
|
||||
|
||||
useEffect(() => () => {
|
||||
Object.values(saveTimersRef.current).forEach(t => window.clearTimeout(t));
|
||||
}, []);
|
||||
|
||||
const persistFns = useMemo(
|
||||
() => ({ saveParams, saveVisual }),
|
||||
[saveParams, saveVisual],
|
||||
);
|
||||
|
||||
const schedulePersist = useCallback((updated: IndicatorConfig) => {
|
||||
const type = updated.type;
|
||||
setConfigs(prev => ({ ...prev, [type]: updated }));
|
||||
window.clearTimeout(saveTimersRef.current[type]);
|
||||
saveTimersRef.current[type] = window.setTimeout(() => {
|
||||
persistIndicatorConfigToDb(updated, persistFns);
|
||||
onPersisted?.(type);
|
||||
}, 200);
|
||||
}, [persistFns, onPersisted]);
|
||||
|
||||
const toggleExpand = useCallback((type: string) => {
|
||||
setExpandedTypes(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(type)) next.delete(type);
|
||||
else next.add(type);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleRowDefaults = useCallback((type: string) => {
|
||||
const base = configs[type];
|
||||
if (!base) return;
|
||||
const reset = resetConfigToDefaults(base);
|
||||
schedulePersist(reset);
|
||||
}, [configs, schedulePersist]);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => filterSettingsIndicatorTypes(indicatorTypes, search),
|
||||
[indicatorTypes, search],
|
||||
);
|
||||
const { main: filteredMain, other: filteredOther } = useMemo(
|
||||
() => partitionFilteredTypes(filtered),
|
||||
[filtered],
|
||||
);
|
||||
|
||||
if (!strategy) {
|
||||
return <p className="btd-sidebar-empty">전략을 먼저 선택하세요</p>;
|
||||
}
|
||||
|
||||
const renderRows = (types: string[]) => types.map(type => {
|
||||
const cfg = configs[type];
|
||||
if (!cfg) return null;
|
||||
return (
|
||||
<IndicatorSettingsListRow
|
||||
key={type}
|
||||
type={type}
|
||||
config={cfg}
|
||||
enabled
|
||||
chartMarket={market}
|
||||
expanded={expandedTypes.has(type)}
|
||||
variant="settings"
|
||||
hideChartToggle
|
||||
hideOutputFooter
|
||||
onToggleExpand={() => toggleExpand(type)}
|
||||
onToggleEnabled={() => {}}
|
||||
onChange={schedulePersist}
|
||||
onRowDefaults={() => handleRowDefaults(type)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="seval-chart-settings-tab">
|
||||
<p className="seval-chart-settings-hint">
|
||||
변경 사항은 저장 버튼 없이 즉시 차트·전역 보조지표 설정에 반영됩니다.
|
||||
</p>
|
||||
<IndicatorSettingsListSearch
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
totalCount={indicatorTypes.length}
|
||||
filteredCount={filtered.length}
|
||||
/>
|
||||
<div className="stg-ind-list seval-chart-settings-list">
|
||||
{filteredMain.length > 0 && (
|
||||
<section className="seval-chart-settings-section">
|
||||
<h3 className="seval-chart-settings-section-title">오버레이 · 주요 지표</h3>
|
||||
{renderRows(filteredMain)}
|
||||
</section>
|
||||
)}
|
||||
{filteredOther.length > 0 && (
|
||||
<section className="seval-chart-settings-section">
|
||||
<h3 className="seval-chart-settings-section-title">전략 사용 지표</h3>
|
||||
{renderRows(filteredOther)}
|
||||
</section>
|
||||
)}
|
||||
{filtered.length === 0 && (
|
||||
<p className="btd-sidebar-empty">검색 결과가 없습니다</p>
|
||||
)}
|
||||
<ChartPriceAxisLabelSettings
|
||||
candleAreaEnabled={candlePriceLabels}
|
||||
indicatorAreaEnabled={indicatorPriceLabels}
|
||||
onCandleAreaChange={v => saveAppSettings({ chartCandleAreaPriceLabels: v })}
|
||||
onIndicatorAreaChange={v => saveAppSettings({ chartSeriesPriceLabels: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyEvaluationChartSettingsTab;
|
||||
Reference in New Issue
Block a user