전략평가 차트설정탭 추가수정
This commit is contained in:
@@ -50,6 +50,8 @@ export interface IndicatorSettingsFormProps {
|
|||||||
chartMarket?: string;
|
chartMarket?: string;
|
||||||
/** embedded: 일괄 설정 카드 안 */
|
/** embedded: 일괄 설정 카드 안 */
|
||||||
variant?: 'modal' | 'embedded';
|
variant?: 'modal' | 'embedded';
|
||||||
|
/** embedded 차트설정 등 — 출력·표시(가격축 라벨·타임프레임) 숨김 */
|
||||||
|
hideOutputFooter?: boolean;
|
||||||
/**
|
/**
|
||||||
* modal: 편집 중 부모 setState 없이 ref만 갱신 (포커스 유지).
|
* modal: 편집 중 부모 setState 없이 ref만 갱신 (포커스 유지).
|
||||||
* embedded: 변경 즉시 onChange 호출.
|
* embedded: 변경 즉시 onChange 호출.
|
||||||
@@ -85,6 +87,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
|||||||
variant = 'modal',
|
variant = 'modal',
|
||||||
deferParentSync = variant === 'modal',
|
deferParentSync = variant === 'modal',
|
||||||
inputsOnly = false,
|
inputsOnly = false,
|
||||||
|
hideOutputFooter = false,
|
||||||
chartHidden = false,
|
chartHidden = false,
|
||||||
onChartHiddenChange,
|
onChartHiddenChange,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -289,7 +292,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!inputsOnly && <SettingsOutputFooter {...footerProps} />}
|
{!inputsOnly && !hideOutputFooter && <SettingsOutputFooter {...footerProps} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -323,7 +326,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
inputsOnly={inputsOnly}
|
inputsOnly={inputsOnly}
|
||||||
showOutputFooter={!inputsOnly}
|
showOutputFooter={!inputsOnly && !hideOutputFooter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -351,7 +354,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
|||||||
chartHidden={chartHidden}
|
chartHidden={chartHidden}
|
||||||
onChartHidden={onChartHiddenChange}
|
onChartHidden={onChartHiddenChange}
|
||||||
inputsOnly={inputsOnly}
|
inputsOnly={inputsOnly}
|
||||||
showOutputFooter={!inputsOnly && !isBollinger}
|
showOutputFooter={!inputsOnly && !hideOutputFooter && !isBollinger}
|
||||||
symbolSection={isBollinger ? (
|
symbolSection={isBollinger ? (
|
||||||
<BollingerSymbolSection
|
<BollingerSymbolSection
|
||||||
symbolMode={String(params.symbolMode ?? 'main')}
|
symbolMode={String(params.symbolMode ?? 'main')}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export interface IndicatorSettingsListRowProps {
|
|||||||
onDragOver?: (e: React.DragEvent) => void;
|
onDragOver?: (e: React.DragEvent) => void;
|
||||||
onDragLeave?: () => void;
|
onDragLeave?: () => void;
|
||||||
onDrop?: (e: React.DragEvent) => void;
|
onDrop?: (e: React.DragEvent) => void;
|
||||||
|
/** 차트 on/off 토글 숨김 (전략 평가 차트설정 등) */
|
||||||
|
hideChartToggle?: boolean;
|
||||||
|
/** 출력·표시(가격축 라벨·타임프레임) 숨김 */
|
||||||
|
hideOutputFooter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||||
@@ -41,6 +45,8 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
|||||||
onDragOver,
|
onDragOver,
|
||||||
onDragLeave,
|
onDragLeave,
|
||||||
onDrop,
|
onDrop,
|
||||||
|
hideChartToggle = false,
|
||||||
|
hideOutputFooter = false,
|
||||||
}) => {
|
}) => {
|
||||||
const labels = getIndicatorListLabels(type);
|
const labels = getIndicatorListLabels(type);
|
||||||
const def = getIndicatorDef(type);
|
const def = getIndicatorDef(type);
|
||||||
@@ -88,12 +94,15 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
|||||||
<div className={badgesClass}>
|
<div className={badgesClass}>
|
||||||
{def?.overlay && <span className="ind-panel-badge">오버레이</span>}
|
{def?.overlay && <span className="ind-panel-badge">오버레이</span>}
|
||||||
{def?.returnsMarkers && <span className="ind-panel-badge">마커</span>}
|
{def?.returnsMarkers && <span className="ind-panel-badge">마커</span>}
|
||||||
{enabled ? (
|
{!hideChartToggle && (
|
||||||
|
enabled ? (
|
||||||
<span className={onBadgeClass}>차트 적용</span>
|
<span className={onBadgeClass}>차트 적용</span>
|
||||||
) : (
|
) : (
|
||||||
<span className={offBadgeClass}>미적용</span>
|
<span className={offBadgeClass}>미적용</span>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{!hideChartToggle && (
|
||||||
<label
|
<label
|
||||||
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
||||||
title={enabled ? '차트에서 제거' : '차트에 추가'}
|
title={enabled ? '차트에서 제거' : '차트에 추가'}
|
||||||
@@ -106,6 +115,7 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
|||||||
/>
|
/>
|
||||||
<span className="ism-toggle-slider" />
|
<span className="ism-toggle-slider" />
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
{variant === 'settings' && onRowDefaults && (
|
{variant === 'settings' && onRowDefaults && (
|
||||||
<button type="button" className="stg-ind-card-defaults" onClick={onRowDefaults}>
|
<button type="button" className="stg-ind-card-defaults" onClick={onRowDefaults}>
|
||||||
기본값
|
기본값
|
||||||
@@ -128,6 +138,7 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
|||||||
config={config}
|
config={config}
|
||||||
chartMarket={chartMarket}
|
chartMarket={chartMarket}
|
||||||
variant="embedded"
|
variant="embedded"
|
||||||
|
hideOutputFooter={hideOutputFooter}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisRep
|
|||||||
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
||||||
import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup';
|
import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup';
|
||||||
import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
|
import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
|
||||||
|
import StrategyEvaluationChartSettingsTab from './strategyEvaluation/StrategyEvaluationChartSettingsTab';
|
||||||
import {
|
import {
|
||||||
buildStrategyEvaluationAiVerifyContext,
|
buildStrategyEvaluationAiVerifyContext,
|
||||||
requestStrategyEvaluationAiVerify,
|
requestStrategyEvaluationAiVerify,
|
||||||
@@ -65,7 +66,7 @@ const LEFT_DEFAULT = 380;
|
|||||||
const RIGHT_DEFAULT = 440;
|
const RIGHT_DEFAULT = 440;
|
||||||
|
|
||||||
type LeftTab = 'strategy' | 'market' | 'settings';
|
type LeftTab = 'strategy' | 'market' | 'settings';
|
||||||
type RightTab = 'signal' | 'ai';
|
type RightTab = 'signal' | 'chart' | 'ai';
|
||||||
|
|
||||||
function readMinWidth(key: string, fallback: number): number {
|
function readMinWidth(key: string, fallback: number): number {
|
||||||
return Math.max(readStoredSize(key, fallback), fallback);
|
return Math.max(readStoredSize(key, fallback), fallback);
|
||||||
@@ -76,7 +77,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function StrategyEvaluationPage({ theme = 'dark' }: 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 [leftTab, setLeftTab] = useState<LeftTab>('strategy');
|
||||||
const [marketSearchOpen, setMarketSearchOpen] = useState(false);
|
const [marketSearchOpen, setMarketSearchOpen] = useState(false);
|
||||||
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
|
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
|
||||||
@@ -413,6 +420,16 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
setParamsRevision(v => v + 1);
|
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
|
const strategyLabel = selectedStrategy?.name
|
||||||
? repairUtf8Mojibake(selectedStrategy.name)
|
? repairUtf8Mojibake(selectedStrategy.name)
|
||||||
: '전략 미선택';
|
: '전략 미선택';
|
||||||
@@ -783,7 +800,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
side="right"
|
side="right"
|
||||||
open={rightOpen}
|
open={rightOpen}
|
||||||
onToggle={toggleRightPanel}
|
onToggle={toggleRightPanel}
|
||||||
label="시그널 패널"
|
label={rightTab === 'chart' ? '차트설정 패널' : '시그널 패널'}
|
||||||
/>
|
/>
|
||||||
{rightOpen && (
|
{rightOpen && (
|
||||||
<div
|
<div
|
||||||
@@ -807,6 +824,17 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
>
|
>
|
||||||
시그널
|
시그널
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
@@ -877,6 +905,26 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
</div>
|
</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' && (
|
{rightTab === 'ai' && (
|
||||||
<div
|
<div
|
||||||
id="seval-right-panel-ai"
|
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';
|
} from '../../utils/chartOverlayVisibility';
|
||||||
import { buildStrategyEvaluationChartIndicators } from '../../utils/strategyToChartIndicators';
|
import { buildStrategyEvaluationChartIndicators } from '../../utils/strategyToChartIndicators';
|
||||||
import { buildEvalParamsFromStrategy } from '../../utils/strategyEvaluationParams';
|
import { buildEvalParamsFromStrategy } from '../../utils/strategyEvaluationParams';
|
||||||
import { indicatorDefaultsFingerprint } from '../../utils/indicatorDefaultsFingerprint';
|
|
||||||
import { repairUtf8Mojibake } from '../../utils/textEncoding';
|
import { repairUtf8Mojibake } from '../../utils/textEncoding';
|
||||||
import StrategyEvaluationBarSelector, {
|
import StrategyEvaluationBarSelector, {
|
||||||
resolveBarIndexAtChartClick,
|
resolveBarIndexAtChartClick,
|
||||||
@@ -150,6 +149,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
const getParams = getParamsOverride ?? baseGetParams;
|
const getParams = getParamsOverride ?? baseGetParams;
|
||||||
const getVisualConfig = getVisualConfigOverride ?? baseGetVisual;
|
const getVisualConfig = getVisualConfigOverride ?? baseGetVisual;
|
||||||
const { defaults: appDefaults } = useAppSettings();
|
const { defaults: appDefaults } = useAppSettings();
|
||||||
|
const candlePriceLabels = appDefaults.chartCandleAreaPriceLabels !== false;
|
||||||
|
const indicatorPriceLabels = appDefaults.chartSeriesPriceLabels !== false;
|
||||||
const [bars, setBars] = useState<OHLCVBar[]>([]);
|
const [bars, setBars] = useState<OHLCVBar[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -297,9 +298,9 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
[strategy, getParams],
|
[strategy, getParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const indicatorRenderKey = useMemo(
|
const indicatorParamsKey = useMemo(
|
||||||
() => indicators.map(i => indicatorDefaultsFingerprint(i)).join('|'),
|
() => JSON.stringify(indicatorParams ?? {}),
|
||||||
[indicators],
|
[indicatorParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolveInitialDisplayCount = useCallback(
|
const resolveInitialDisplayCount = useCallback(
|
||||||
@@ -382,7 +383,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
void runSignalScanForBars(bars);
|
void runSignalScanForBars(bars);
|
||||||
return () => { ++backtestGenRef.current; };
|
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(() => {
|
useEffect(() => {
|
||||||
applyMarkers();
|
applyMarkers();
|
||||||
@@ -694,7 +695,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
{!loading && !error && bars.length > 0 && (
|
{!loading && !error && bars.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<TradingChart
|
<TradingChart
|
||||||
key={`${market}-${timeframe}-${strategy?.id ?? 0}-${paramsRevision}-${settingsRevision}-${indicatorRenderKey}`}
|
key={`${market}-${timeframe}-${strategy?.id ?? 0}`}
|
||||||
bars={bars}
|
bars={bars}
|
||||||
barsMarket={market}
|
barsMarket={market}
|
||||||
market={market}
|
market={market}
|
||||||
@@ -720,6 +721,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
crosshairInfoVisible={false}
|
crosshairInfoVisible={false}
|
||||||
showHoverToolbar={false}
|
showHoverToolbar={false}
|
||||||
showChartRightToolbar={false}
|
showChartRightToolbar={false}
|
||||||
|
candleAreaPriceLabelsEnabled={candlePriceLabels}
|
||||||
|
indicatorAreaPriceLabelsEnabled={indicatorPriceLabels}
|
||||||
showCandlePaneControls={false}
|
showCandlePaneControls={false}
|
||||||
showCandleOverlayControls
|
showCandleOverlayControls
|
||||||
candleOverlayToggles={candleOverlayToggles}
|
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;
|
||||||
@@ -1173,6 +1173,206 @@
|
|||||||
overflow: hidden;
|
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 검증 우측 패널 탭 ── */
|
/* ── AI 검증 우측 패널 탭 ── */
|
||||||
.seval-ai-verify-panel {
|
.seval-ai-verify-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -233,3 +233,31 @@ export function resetConfigToDefaults(config: IndicatorConfig): IndicatorConfig
|
|||||||
};
|
};
|
||||||
return enrichIndicatorConfig(draft);
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user