goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,595 @@
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 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 ? (
<ColorInput
value={plot.color}
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 }) => (
<>
<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 .
</p>
<div className="ism-ichimoku-cloud-grid">
<div className="ism-ichimoku-cloud-card">
<span className="ism-ichimoku-cloud-card-label"> (1 &gt; 2)</span>
<div className="ism-ichimoku-cloud-card-row">
<ColorInput
value={cloudColors.bullishColor}
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">
<span className="ism-ichimoku-cloud-card-label"> (1 &lt; 2)</span>
<div className="ism-ichimoku-cloud-card-row">
<ColorInput
value={cloudColors.bearishColor}
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>
);
};