644 lines
23 KiB
TypeScript
644 lines
23 KiB
TypeScript
import React from 'react';
|
|
import type { IndicatorConfig, HlinesBackground, Timeframe } from '../types';
|
|
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
|
import { getHLineLabel } from '../utils/indicatorRegistry';
|
|
import { getParamLabel, getPlotLabel, getSrcOptionLabel } from '../utils/indicatorLabels';
|
|
import { getIchimokuPlotTitle } from '../utils/ichimokuConfig';
|
|
import {
|
|
getNumericParamSpec,
|
|
getHlinePriceSpec,
|
|
} from '../utils/indicatorParamSpec';
|
|
import {
|
|
getGlobalParamKeys,
|
|
getPlotParamKeys,
|
|
plotParamsOnGlobalRowOnly,
|
|
plotRowHasNoInputs,
|
|
} from '../utils/indicatorSettingsLayout';
|
|
import NumericParamInput from './NumericParamInput';
|
|
import PlotLineStylePicker from './PlotLineStylePicker';
|
|
import ColorInput from './ColorInput';
|
|
import { colorToRgbaString, cloudColorOpacityPercent } from '../utils/ichimokuConfig';
|
|
import { resolveHistogramUpColor, resolveHistogramDownColor } from '../utils/plotColorUtils';
|
|
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
|
|
|
|
const SRC_OPTIONS = ['close', 'open', 'high', 'low', 'hl2', 'hlc3', 'ohlc4'];
|
|
|
|
const MA_TYPE_OPTIONS_CCI = ['None', 'SMA', 'EMA', 'WMA'] as const;
|
|
const MA_TYPE_OPTIONS_OBV = ['SMA', 'EMA', 'WMA'] as const;
|
|
|
|
/** 업비트 OBV 드롭다운 표기 */
|
|
const OBV_MA_TYPE_LABELS: Record<string, string> = {
|
|
SMA: '단순이동평균',
|
|
EMA: '지수 이동 평균',
|
|
WMA: '가중이동평균',
|
|
};
|
|
|
|
const MA_TYPE_LABELS: Record<string, string> = {
|
|
None: '없음 (None)',
|
|
SMA: '단순이동평균 (SMA)',
|
|
EMA: '지수이동평균 (EMA)',
|
|
WMA: '가중이동평균 (WMA)',
|
|
'SMMA (RMA)': 'RMA (SMMA)',
|
|
VWMA: '거래량가중이동평균 (VWMA)',
|
|
};
|
|
|
|
function isMaTypeParam(indicatorType: string, key: string, value: unknown): boolean {
|
|
if (key !== 'maType' || typeof value !== 'string') return false;
|
|
return indicatorType === 'CCI' || indicatorType === 'OBV' || indicatorType === 'RSI';
|
|
}
|
|
|
|
function maTypeOptionsFor(indicatorType: string): readonly string[] {
|
|
if (indicatorType === 'OBV') return MA_TYPE_OPTIONS_OBV;
|
|
return MA_TYPE_OPTIONS_CCI;
|
|
}
|
|
|
|
function isSrcParam(key: string, value: unknown): boolean {
|
|
return (key === 'src' || key.toLowerCase().includes('src')) && typeof value === 'string';
|
|
}
|
|
|
|
export { ColorInput };
|
|
|
|
/* ── 단일 파라미터 컨트롤 ─────────────────────────────────────── */
|
|
export const ParamField: React.FC<{
|
|
indicatorType: string;
|
|
paramKey: string;
|
|
value: number | string | boolean;
|
|
disabled?: boolean;
|
|
onChange: (key: string, raw: string | boolean) => void;
|
|
}> = ({ indicatorType, paramKey, value, disabled, onChange }) => {
|
|
if (typeof value === 'boolean') {
|
|
return (
|
|
<label className="ism-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={value}
|
|
disabled={disabled}
|
|
onChange={e => onChange(paramKey, e.target.checked)}
|
|
/>
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
);
|
|
}
|
|
if (isMaTypeParam(indicatorType, paramKey, value)) {
|
|
return (
|
|
<select
|
|
className="ism-select ism-param-inline-select"
|
|
value={value as string}
|
|
disabled={disabled}
|
|
onChange={e => onChange(paramKey, e.target.value)}
|
|
>
|
|
{maTypeOptionsFor(indicatorType).map(o => (
|
|
<option key={o} value={o}>
|
|
{indicatorType === 'OBV'
|
|
? (OBV_MA_TYPE_LABELS[o] ?? o)
|
|
: (MA_TYPE_LABELS[o] ?? o)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
);
|
|
}
|
|
if (isSrcParam(paramKey, value)) {
|
|
return (
|
|
<select
|
|
className="ism-select ism-param-inline-select"
|
|
value={value as string}
|
|
disabled={disabled}
|
|
onChange={e => onChange(paramKey, e.target.value)}
|
|
>
|
|
{SRC_OPTIONS.map(o => (
|
|
<option key={o} value={o}>{getSrcOptionLabel(o)}</option>
|
|
))}
|
|
</select>
|
|
);
|
|
}
|
|
if (typeof value === 'number') {
|
|
return (
|
|
<NumericParamInput
|
|
className="ism-input ism-narrow ism-sma-period-input"
|
|
value={value}
|
|
spec={getNumericParamSpec(indicatorType, paramKey, value)}
|
|
disabled={disabled}
|
|
onChange={v => onChange(paramKey, String(v))}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<input
|
|
type="text"
|
|
className="ism-input ism-narrow"
|
|
value={value as string}
|
|
disabled={disabled}
|
|
onChange={e => onChange(paramKey, e.target.value)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
/* ── 플롯 설정 행 (on/off · 이름 · 입력 · 색상·선) ─────────────── */
|
|
export const PlotSettingsRow: React.FC<{
|
|
indicatorType: string;
|
|
plot: PlotDef;
|
|
plotIndex: number;
|
|
plots: PlotDef[];
|
|
params: Record<string, number | string | boolean>;
|
|
enabled: boolean;
|
|
inputsOnly?: boolean;
|
|
onToggle: (plotId: string, on: boolean) => void;
|
|
onParamChange: (key: string, raw: string | boolean) => void;
|
|
onPlotStyle: (plotIndex: number, patch: Partial<PlotDef>) => void;
|
|
}> = ({
|
|
indicatorType, plot, plotIndex, plots, params, enabled, inputsOnly,
|
|
onToggle, onParamChange, onPlotStyle,
|
|
}) => {
|
|
const paramKeys = getPlotParamKeys(indicatorType, plot.id, plotIndex, plots, params);
|
|
const noInputs = plotRowHasNoInputs(indicatorType, plot.id, plotIndex, plots, params);
|
|
const globalOnlyInputs = plotParamsOnGlobalRowOnly(indicatorType, plot.id);
|
|
const label = indicatorType === 'IchimokuCloud'
|
|
? getIchimokuPlotTitle(plot.id)
|
|
: getPlotLabel(plot.title);
|
|
const isHistogram = plot.type === 'histogram';
|
|
|
|
return (
|
|
<div className={`ism-row ism-sma-ma-row${enabled ? '' : ' ism-sma-ma-off'}`}>
|
|
<label className="ism-toggle ism-sma-ma-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={enabled}
|
|
onChange={e => onToggle(plot.id, e.target.checked)}
|
|
/>
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
<label className="ism-label ism-sma-ma-label">{label}</label>
|
|
<div className="ism-control ism-sma-ma-control">
|
|
{noInputs && globalOnlyInputs ? (
|
|
<span className="ism-hlines-bg-spacer" aria-hidden />
|
|
) : noInputs ? (
|
|
<span className="ism-ichimoku-auto-param" title="상위 설정 또는 자동 계산">자동</span>
|
|
) : (
|
|
<div className="ism-plot-param-inputs">
|
|
{paramKeys.map(key => (
|
|
<ParamField
|
|
key={key}
|
|
indicatorType={indicatorType}
|
|
paramKey={key}
|
|
value={params[key]}
|
|
disabled={!enabled}
|
|
onChange={onParamChange}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
{!inputsOnly && (
|
|
isHistogram && indicatorType === 'MACD' ? (
|
|
<div className="ism-hist-colors">
|
|
<div className="ism-hist-color-field">
|
|
<span className="ism-style-label">0선 위</span>
|
|
<ColorInput
|
|
value={resolveHistogramUpColor(plot)}
|
|
disabled={!enabled}
|
|
onChange={v => onPlotStyle(plotIndex, { histogramUpColor: v, color: v })}
|
|
/>
|
|
</div>
|
|
<div className="ism-hist-color-field">
|
|
<span className="ism-style-label">0선 아래</span>
|
|
<ColorInput
|
|
value={resolveHistogramDownColor(plot)}
|
|
disabled={!enabled}
|
|
onChange={v => onPlotStyle(plotIndex, { histogramDownColor: v })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : isHistogram ? (
|
|
<ColorInput
|
|
value={plot.color}
|
|
disabled={!enabled}
|
|
onChange={v => onPlotStyle(plotIndex, { color: v })}
|
|
/>
|
|
) : (
|
|
<PlotLineStylePicker
|
|
disabled={!enabled}
|
|
title={label}
|
|
value={{
|
|
color: plot.color,
|
|
lineWidth: plot.lineWidth,
|
|
lineStyle: plot.lineStyle,
|
|
}}
|
|
onChange={patch => onPlotStyle(plotIndex, patch)}
|
|
/>
|
|
)
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/* ── 전역 파라미터 행 (토글 없음) ─────────────────────────────── */
|
|
export const GlobalParamRow: React.FC<{
|
|
indicatorType: string;
|
|
paramKey: string;
|
|
value: number | string | boolean;
|
|
onChange: (key: string, raw: string | boolean) => void;
|
|
}> = ({ indicatorType, paramKey, value, onChange }) => (
|
|
<div className="ism-row ism-global-param-row">
|
|
<span className="ism-label ism-sma-ma-label">{getParamLabel(paramKey, indicatorType)}</span>
|
|
<div className="ism-control ism-sma-ma-control">
|
|
<ParamField
|
|
indicatorType={indicatorType}
|
|
paramKey={paramKey}
|
|
value={value}
|
|
onChange={onChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
/* ── 수평선 행 ───────────────────────────────────────────────── */
|
|
export const HLineSettingsRow: React.FC<{
|
|
indicatorType: string;
|
|
hl: HLineDef;
|
|
idx: number;
|
|
allPrices: number[];
|
|
inputsOnly?: boolean;
|
|
onVisible: (idx: number, v: boolean) => void;
|
|
onPrice: (idx: number, v: number) => void;
|
|
onStyle: (idx: number, patch: Partial<HLineDef>) => void;
|
|
}> = ({ indicatorType, hl, idx, allPrices, inputsOnly, onVisible, onPrice, onStyle }) => {
|
|
const label = hl.label ?? getHLineLabel(hl.price, allPrices);
|
|
const isOn = hl.visible !== false;
|
|
return (
|
|
<div className={`ism-row ism-sma-ma-row${isOn ? '' : ' ism-sma-ma-off'}`}>
|
|
<label className="ism-toggle ism-sma-ma-toggle">
|
|
<input type="checkbox" checked={isOn} onChange={e => onVisible(idx, e.target.checked)} />
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
<label className="ism-label ism-sma-ma-label">{label}</label>
|
|
<div className="ism-control ism-sma-ma-control">
|
|
<NumericParamInput
|
|
className="ism-input ism-narrow ism-sma-period-input"
|
|
value={hl.price}
|
|
spec={getHlinePriceSpec(indicatorType, hl.price)}
|
|
disabled={!isOn}
|
|
onChange={v => onPrice(idx, v)}
|
|
/>
|
|
{!inputsOnly && (
|
|
<PlotLineStylePicker
|
|
disabled={!isOn}
|
|
title={label}
|
|
value={{
|
|
color: hl.color,
|
|
lineWidth: hl.lineWidth,
|
|
lineStyle: hl.lineStyle ?? 'dashed',
|
|
}}
|
|
onChange={patch => onStyle(idx, patch)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/* ── 출력 · 타임프레임 (하단 공통) ───────────────────────────── */
|
|
export const SettingsOutputFooter: React.FC<{
|
|
lastValueVisible: boolean;
|
|
onLastValueVisible: (v: boolean) => void;
|
|
timeframeVis: Partial<Record<Timeframe, boolean>>;
|
|
onTimeframeVis: (tf: Timeframe, v: boolean) => void;
|
|
timeframes: { tf: Timeframe; label: string }[];
|
|
/** 개별 설정 모달: 지표 전체 숨김 */
|
|
chartHidden?: boolean;
|
|
onChartHidden?: (showOnChart: boolean) => void;
|
|
}> = ({
|
|
lastValueVisible, onLastValueVisible, timeframeVis, onTimeframeVis, timeframes,
|
|
chartHidden, onChartHidden,
|
|
}) => (
|
|
<>
|
|
<div className="ism-ichimoku-cloud-divider" />
|
|
<div className="ism-output-section-title">출력 · 표시</div>
|
|
{onChartHidden != null && (
|
|
<div className="ism-row ism-ichimoku-extra-row">
|
|
<span className="ism-label">차트에 표시</span>
|
|
<div className="ism-control">
|
|
<label className="ism-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={chartHidden !== true}
|
|
onChange={e => onChartHidden(e.target.checked)}
|
|
/>
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="ism-row ism-ichimoku-extra-row">
|
|
<span className="ism-label">가격축 라벨·설명</span>
|
|
<div className="ism-control">
|
|
<label className="ism-toggle" title="이 지표만 우측 가격축 금액·설명 표시 (해당 영역·차트 설정 전역 스위치가 켜져 있을 때 적용)">
|
|
<input
|
|
type="checkbox"
|
|
checked={lastValueVisible}
|
|
onChange={e => onLastValueVisible(e.target.checked)}
|
|
/>
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div className="ism-tf-grid ism-ichimoku-tf-grid">
|
|
{timeframes.map(({ tf, label }) => (
|
|
<label key={tf} className="ism-tf-item">
|
|
<input
|
|
type="checkbox"
|
|
checked={timeframeVis[tf] !== false}
|
|
onChange={e => onTimeframeVis(tf, e.target.checked)}
|
|
/>
|
|
<span className="ism-tf-label">{label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
/* ── 일목 구름 색상 ─────────────────────────────────────────── */
|
|
export const IchimokuCloudSection: React.FC<{
|
|
cloudColors: IchimokuCloudColors;
|
|
onChange: (patch: Partial<IchimokuCloudColors>) => void;
|
|
}> = ({ cloudColors, onChange }) => {
|
|
const bullishOn = cloudColors.bullishVisible !== false;
|
|
const bearishOn = cloudColors.bearishVisible !== false;
|
|
|
|
return (
|
|
<>
|
|
<div className="ism-ichimoku-cloud-divider" />
|
|
<div className="ism-ichimoku-cloud-section">
|
|
<h4 className="ism-ichimoku-cloud-title">구름 영역</h4>
|
|
<p className="ism-ichimoku-cloud-hint">
|
|
선행스팬1·선행스팬2가 모두 켜져 있을 때만 구름을 그릴 수 있습니다. 상승·하락 구름은 각각 on/off 할 수 있습니다.
|
|
</p>
|
|
<div className="ism-ichimoku-cloud-grid">
|
|
<div className={`ism-ichimoku-cloud-card${bullishOn ? '' : ' ism-ichimoku-cloud-card--off'}`}>
|
|
<div className="ism-ichimoku-cloud-card-head">
|
|
<label className="ism-toggle ism-ichimoku-cloud-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={bullishOn}
|
|
onChange={e => onChange({ bullishVisible: e.target.checked })}
|
|
/>
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
<span className="ism-ichimoku-cloud-card-label">상승 구름 (선행1 > 선행2)</span>
|
|
</div>
|
|
<div className="ism-ichimoku-cloud-card-row">
|
|
<ColorInput
|
|
value={cloudColors.bullishColor}
|
|
disabled={!bullishOn}
|
|
onChange={v => onChange({ bullishColor: v })}
|
|
/>
|
|
</div>
|
|
<div className="ism-ichimoku-cloud-meta">
|
|
{colorToRgbaString(cloudColors.bullishColor)}
|
|
{' · '}투명도 {cloudColorOpacityPercent(cloudColors.bullishColor)}%
|
|
</div>
|
|
</div>
|
|
<div className={`ism-ichimoku-cloud-card${bearishOn ? '' : ' ism-ichimoku-cloud-card--off'}`}>
|
|
<div className="ism-ichimoku-cloud-card-head">
|
|
<label className="ism-toggle ism-ichimoku-cloud-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={bearishOn}
|
|
onChange={e => onChange({ bearishVisible: e.target.checked })}
|
|
/>
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
<span className="ism-ichimoku-cloud-card-label">하락 구름 (선행1 < 선행2)</span>
|
|
</div>
|
|
<div className="ism-ichimoku-cloud-card-row">
|
|
<ColorInput
|
|
value={cloudColors.bearishColor}
|
|
disabled={!bearishOn}
|
|
onChange={v => onChange({ bearishColor: v })}
|
|
/>
|
|
</div>
|
|
<div className="ism-ichimoku-cloud-meta">
|
|
{colorToRgbaString(cloudColors.bearishColor)}
|
|
{' · '}투명도 {cloudColorOpacityPercent(cloudColors.bearishColor)}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
/* ── 볼린저밴드 백그라운드 (업비트 모습 탭) ───────────────────── */
|
|
export const BollingerBackgroundRow: React.FC<{
|
|
value: HlinesBackground;
|
|
onChange: (v: HlinesBackground) => void;
|
|
}> = ({ value, onChange }) => (
|
|
<div className={`ism-row ism-sma-ma-row${value.visible ? '' : ' ism-sma-ma-off'}`}>
|
|
<label className="ism-toggle ism-sma-ma-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={value.visible}
|
|
onChange={e => onChange({ ...value, visible: e.target.checked })}
|
|
/>
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
<span className="ism-label ism-sma-ma-label">백그라운드 그리기</span>
|
|
<div className="ism-control ism-sma-ma-control">
|
|
<span className="ism-ichimoku-auto-param ism-hlines-bg-spacer" aria-hidden />
|
|
<ColorInput
|
|
value={value.color}
|
|
onChange={c => onChange({ ...value, color: c })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
/* ── 수평선 배경 ─────────────────────────────────────────────── */
|
|
export const HlinesBackgroundRow: React.FC<{
|
|
value: HlinesBackground;
|
|
onChange: (v: HlinesBackground) => void;
|
|
}> = ({ value, onChange }) => (
|
|
<>
|
|
<div className="ism-ichimoku-cloud-divider" />
|
|
<div className="ism-row ism-sma-ma-row">
|
|
<label className="ism-toggle ism-sma-ma-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={value.visible}
|
|
onChange={e => onChange({ ...value, visible: e.target.checked })}
|
|
/>
|
|
<span className="ism-toggle-slider" />
|
|
</label>
|
|
<span className="ism-label ism-sma-ma-label">수평선 배경</span>
|
|
<div className="ism-control ism-sma-ma-control">
|
|
<span className="ism-ichimoku-auto-param ism-hlines-bg-spacer" aria-hidden />
|
|
<ColorInput
|
|
value={value.color}
|
|
onChange={c => onChange({ ...value, color: c })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
/* ── 볼린저밴드 심볼 (업비트 인풋) ───────────────────────────── */
|
|
export const BollingerSymbolSection: React.FC<{
|
|
symbolMode: string;
|
|
refSymbol: string;
|
|
chartMarket: string;
|
|
onChange: (patch: { symbolMode?: string; refSymbol?: string }) => void;
|
|
}> = ({ symbolMode, refSymbol, chartMarket, onChange }) => {
|
|
const isOther = symbolMode === 'other';
|
|
return (
|
|
<div className="ism-bb-symbol-section">
|
|
<div className="ism-output-section-title">심볼</div>
|
|
<label className="ism-bb-symbol-option">
|
|
<input
|
|
type="radio"
|
|
name="bb-symbol-mode"
|
|
checked={!isOther}
|
|
onChange={() => onChange({ symbolMode: 'main' })}
|
|
/>
|
|
<span>메인 차트 심볼</span>
|
|
{chartMarket ? <span className="ism-bb-symbol-hint">{chartMarket}</span> : null}
|
|
</label>
|
|
<label className="ism-bb-symbol-option ism-bb-symbol-other">
|
|
<input
|
|
type="radio"
|
|
name="bb-symbol-mode"
|
|
checked={isOther}
|
|
onChange={() => onChange({ symbolMode: 'other' })}
|
|
/>
|
|
<span>다른 심볼</span>
|
|
<input
|
|
type="text"
|
|
className="ism-input ism-bb-ref-symbol"
|
|
placeholder="KRW-BTC"
|
|
value={refSymbol}
|
|
disabled={!isOther}
|
|
onChange={e => onChange({ refSymbol: e.target.value.toUpperCase() })}
|
|
/>
|
|
</label>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/* ── 통합 본문 (일반 지표) ───────────────────────────────────── */
|
|
export const UnifiedIndicatorBody: React.FC<{
|
|
config: IndicatorConfig;
|
|
params: Record<string, number | string | boolean>;
|
|
plots: PlotDef[];
|
|
plotVis: Record<string, boolean>;
|
|
hlines: HLineDef[];
|
|
hlinesBg: HlinesBackground;
|
|
hasHLines: boolean;
|
|
lastValueVisible: boolean;
|
|
timeframeVis: Partial<Record<Timeframe, boolean>>;
|
|
timeframes: { tf: Timeframe; label: string }[];
|
|
onParamChange: (key: string, raw: string | boolean) => void;
|
|
onPlotToggle: (plotId: string, on: boolean) => void;
|
|
onPlotStyle: (idx: number, patch: Partial<PlotDef>) => void;
|
|
onHLineVisible: (idx: number, v: boolean) => void;
|
|
onHLinePrice: (idx: number, v: number) => void;
|
|
onHLineStyle: (idx: number, patch: Partial<HLineDef>) => void;
|
|
onHlinesBg: (v: HlinesBackground) => void;
|
|
onLastValueVisible: (v: boolean) => void;
|
|
onTimeframeVis: (tf: Timeframe, v: boolean) => void;
|
|
cloudSection?: React.ReactNode;
|
|
symbolSection?: React.ReactNode;
|
|
/** 플롯 행 아래 추가 섹션 (BB 백그라운드 등) */
|
|
afterPlotsSection?: React.ReactNode;
|
|
/** false면 가격 스케일·타임프레임 보임 설정 숨김 (BB) */
|
|
showOutputFooter?: boolean;
|
|
inputsOnly?: boolean;
|
|
chartHidden?: boolean;
|
|
onChartHidden?: (showOnChart: boolean) => void;
|
|
}> = (props) => {
|
|
const {
|
|
config, params, plots, plotVis, hlines, hlinesBg, hasHLines,
|
|
lastValueVisible, timeframeVis, timeframes,
|
|
onParamChange, onPlotToggle, onPlotStyle,
|
|
onHLineVisible, onHLinePrice, onHLineStyle, onHlinesBg,
|
|
onLastValueVisible, onTimeframeVis, cloudSection, symbolSection,
|
|
afterPlotsSection,
|
|
showOutputFooter = true,
|
|
inputsOnly = false,
|
|
chartHidden,
|
|
onChartHidden,
|
|
} = props;
|
|
|
|
const globalKeys = getGlobalParamKeys(config.type, params, plots);
|
|
const hlinePrices = hlines.map(h => h.price);
|
|
|
|
return (
|
|
<div className="ism-section unified-settings-section">
|
|
{symbolSection}
|
|
{symbolSection && globalKeys.length > 0 && <div className="ism-sma-divider" />}
|
|
{globalKeys.map(key => (
|
|
<GlobalParamRow
|
|
key={key}
|
|
indicatorType={config.type}
|
|
paramKey={key}
|
|
value={params[key]}
|
|
onChange={onParamChange}
|
|
/>
|
|
))}
|
|
|
|
{globalKeys.length > 0 && plots.length > 0 && <div className="ism-sma-divider" />}
|
|
|
|
{plots.map((plot, idx) => (
|
|
<PlotSettingsRow
|
|
key={plot.id}
|
|
indicatorType={config.type}
|
|
plot={plot}
|
|
plotIndex={idx}
|
|
plots={plots}
|
|
params={params}
|
|
enabled={plotVis[plot.id] !== false}
|
|
onToggle={onPlotToggle}
|
|
onParamChange={onParamChange}
|
|
onPlotStyle={onPlotStyle}
|
|
inputsOnly={inputsOnly}
|
|
/>
|
|
))}
|
|
|
|
{cloudSection}
|
|
|
|
{afterPlotsSection}
|
|
|
|
{hlines.length > 0 && (
|
|
<>
|
|
<div className="ism-sma-divider" />
|
|
<div className="ism-output-section-title">수평선</div>
|
|
{hlines.map((hl, idx) => (
|
|
<HLineSettingsRow
|
|
key={idx}
|
|
indicatorType={config.type}
|
|
hl={hl}
|
|
idx={idx}
|
|
allPrices={hlinePrices}
|
|
onVisible={onHLineVisible}
|
|
onPrice={onHLinePrice}
|
|
onStyle={onHLineStyle}
|
|
inputsOnly={inputsOnly}
|
|
/>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{hasHLines && !inputsOnly && <HlinesBackgroundRow value={hlinesBg} onChange={onHlinesBg} />}
|
|
|
|
{showOutputFooter && (
|
|
<SettingsOutputFooter
|
|
lastValueVisible={lastValueVisible}
|
|
onLastValueVisible={onLastValueVisible}
|
|
timeframeVis={timeframeVis}
|
|
onTimeframeVis={onTimeframeVis}
|
|
timeframes={timeframes}
|
|
chartHidden={chartHidden}
|
|
onChartHidden={onChartHidden}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|