전략평가 차트설정탭 추가수정

This commit is contained in:
Macbook
2026-06-18 09:38:10 +09:00
parent 914afc9d5a
commit 615f509f7d
8 changed files with 607 additions and 28 deletions
@@ -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,12 +94,15 @@ 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 ? (
{!hideChartToggle && (
enabled ? (
<span className={onBadgeClass}> </span>
) : (
<span className={offBadgeClass}></span>
)
)}
</div>
{!hideChartToggle && (
<label
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
title={enabled ? '차트에서 제거' : '차트에 추가'}
@@ -106,6 +115,7 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
/>
<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;
+200
View File
@@ -1173,6 +1173,206 @@
overflow: hidden;
}
.seval-right-tab-panel--chart {
overflow: hidden;
}
.seval-chart-settings-tab {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 2px 8px;
}
.seval-chart-settings-hint {
margin: 0;
font-size: 0.68rem;
line-height: 1.45;
color: var(--se-muted, var(--btd-muted));
flex-shrink: 0;
}
.seval-chart-settings-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 2px;
}
.seval-chart-settings-section {
margin-bottom: 12px;
}
.seval-chart-settings-section-title {
margin: 0 0 8px;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--se-muted, var(--btd-muted));
}
/* 차트설정 탭 — 좁은 우측 패널: 라벨 열 여백 축소, 선굵기·선유형까지 한 줄 표시 */
.seval-chart-settings-tab .stg-ind-card {
overflow: visible;
}
.seval-chart-settings-tab .stg-ind-card-head {
padding: 8px 8px;
gap: 6px;
}
.seval-chart-settings-tab .stg-ind-card-body {
padding: 6px 4px 10px;
}
.seval-chart-settings-tab .unified-settings-section {
--ism-row-label-width: max-content;
padding-bottom: 4px;
}
.seval-chart-settings-tab .unified-settings-section .ism-sma-ma-row {
grid-template-columns: 34px max-content minmax(0, 1fr);
gap: 4px 6px;
}
.seval-chart-settings-tab .unified-settings-section .ism-global-param-row {
grid-template-columns: max-content minmax(0, 1fr);
gap: 4px 6px;
}
.seval-chart-settings-tab .unified-settings-section .ism-sma-ma-label {
max-width: 7.5rem;
font-size: 12px;
}
.seval-chart-settings-tab .unified-settings-section .ism-sma-ma-control {
justify-content: flex-start;
gap: 4px;
flex-wrap: nowrap;
overflow: visible;
}
.seval-chart-settings-tab .ism-sma-period-input {
width: 56px;
min-width: 56px;
}
.seval-chart-settings-tab .plsp-inline {
gap: 4px;
}
.seval-chart-settings-tab .plsp-inline-select {
min-width: 40px;
padding: 0 4px;
font-size: 11px;
}
.seval-chart-settings-tab .plsp-inline-select--style {
min-width: 52px;
}
.seval-chart-settings-tab .plsp-swatch {
width: 26px;
height: 26px;
}
.seval-chart-settings-tab .ism-select.ism-narrow {
width: 80px;
min-width: 80px;
}
.seval-chart-settings-tab .ism-param-inline-select {
max-width: 100%;
min-width: 0;
}
.seval-chart-settings-tab .ind-settings-search-input {
font-size: 12px;
}
/* 지표 가격축 라벨·설명 — 목록 하단 전역 설정 */
.seval-price-label-settings {
margin-top: 4px;
padding-top: 12px;
border-top: 1px solid var(--btd-divider, var(--se-border));
}
.seval-price-label-settings-title {
margin: 0 0 8px;
font-size: 0.72rem;
font-weight: 700;
color: var(--se-text, var(--text));
}
.seval-price-label-settings-body {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 6px 4px;
border-radius: 8px;
background: color-mix(in srgb, var(--btd-surface, var(--bg2)) 92%, transparent);
border: 1px solid var(--btd-divider, var(--se-border));
}
.seval-price-label-subhead {
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px 2px 2px;
font-size: 0.68rem;
font-weight: 600;
color: var(--se-text, var(--text));
}
.seval-price-label-subhead--spaced {
margin-top: 6px;
padding-top: 8px;
border-top: 1px solid var(--btd-divider, var(--se-border));
}
.seval-price-label-subdesc {
font-size: 0.62rem;
font-weight: 400;
line-height: 1.4;
color: var(--se-muted, var(--btd-muted));
}
.seval-price-label-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
padding: 6px 2px 8px;
}
.seval-price-label-row-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.seval-price-label-row-title {
font-size: 0.68rem;
font-weight: 600;
color: var(--se-text, var(--text));
}
.seval-price-label-row-desc {
font-size: 0.6rem;
line-height: 1.4;
color: var(--se-muted, var(--btd-muted));
}
.seval-price-label-toggle {
flex-shrink: 0;
margin-top: 2px;
}
/* ── AI 검증 우측 패널 탭 ── */
.seval-ai-verify-panel {
flex: 1;
@@ -233,3 +233,31 @@ export function resetConfigToDefaults(config: IndicatorConfig): IndicatorConfig
};
return enrichIndicatorConfig(draft);
}
export interface PersistIndicatorDefaultsFns {
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;
}
/** 보조지표 설정 1건을 DB 기본값(파라미터·시각)에 즉시 반영 */
export function persistIndicatorConfigToDb(
config: IndicatorConfig,
fns: PersistIndicatorDefaultsFns,
): void {
fns.saveParams(config.type, config.params);
fns.saveVisual(
config.type,
config.plots,
config.hlines,
config.cloudColors,
config.bandBackground,
config.plotVisibility,
);
}