단순이동평균선 이름 표시방식 변경
This commit is contained in:
@@ -1342,31 +1342,19 @@ public class StrategyDslToTa4jAdapter {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** live-conditions·해석 UI — MA5(60일) 형식 */
|
||||
/** live-conditions·해석 UI — MA20일 형식 (슬롯·기간 설정값 기준) */
|
||||
public String formatMovingAverageFieldLabel(String field, Map<String, Map<String, Object>> params) {
|
||||
if (field == null || field.isBlank()) return field;
|
||||
Map<String, Object> sma = params != null ? params.getOrDefault("SMA", Map.of()) : Map.of();
|
||||
if (field.startsWith("MA") && !field.startsWith("MACD")) {
|
||||
int period = resolveMovingAveragePeriod(field, "MA", sma);
|
||||
if (period <= 0) return field;
|
||||
try {
|
||||
int n = Integer.parseInt(field.substring(2));
|
||||
if (n >= 1 && n <= MA_DSL_SLOT_MAX) return "MA" + n + "(" + period + "일)";
|
||||
return "MA(" + period + "일)";
|
||||
} catch (NumberFormatException e) {
|
||||
return field;
|
||||
}
|
||||
return "MA" + period + "일";
|
||||
}
|
||||
if (field.startsWith("EMA")) {
|
||||
int period = resolveMovingAveragePeriod(field, "EMA", sma);
|
||||
if (period <= 0) return field;
|
||||
try {
|
||||
int n = Integer.parseInt(field.substring(3));
|
||||
if (n >= 1 && n <= MA_DSL_SLOT_MAX) return "EMA" + n + "(" + period + "일)";
|
||||
return "EMA(" + period + "일)";
|
||||
} catch (NumberFormatException e) {
|
||||
return field;
|
||||
}
|
||||
return "EMA" + period + "일";
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -45,15 +45,15 @@ class MaDslFieldAdapterTest {
|
||||
void formatMovingAverageFieldLabel_slotLabels() {
|
||||
Map<String, Map<String, Object>> params = Map.of(
|
||||
"SMA", Map.of("period3", 20, "period5", 60));
|
||||
assertEquals("MA3(20일)", adapter.formatMovingAverageFieldLabel("MA3", params));
|
||||
assertEquals("MA5(60일)", adapter.formatMovingAverageFieldLabel("MA5", params));
|
||||
assertEquals("MA20일", adapter.formatMovingAverageFieldLabel("MA3", params));
|
||||
assertEquals("MA60일", adapter.formatMovingAverageFieldLabel("MA5", params));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatMovingAverageFieldLabel_legacyLabels() {
|
||||
Map<String, Map<String, Object>> params = Map.of(
|
||||
"SMA", Map.of("period3", 20, "period5", 60));
|
||||
assertEquals("MA(20일)", adapter.formatMovingAverageFieldLabel("MA20", params));
|
||||
assertEquals("MA(60일)", adapter.formatMovingAverageFieldLabel("MA60", params));
|
||||
assertEquals("MA20일", adapter.formatMovingAverageFieldLabel("MA20", params));
|
||||
assertEquals("MA60일", adapter.formatMovingAverageFieldLabel("MA60", params));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,6 +826,7 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
||||
selection={customOverlaySelection}
|
||||
onSave={sel => handleSaveCustomOverlaySelection('custom', sel)}
|
||||
onCancel={() => setCustomOverlaySettingsSlot(null)}
|
||||
smaParams={indicators.find(i => i.type === 'SMA')?.params}
|
||||
/>
|
||||
)}
|
||||
{customOverlaySettingsSlot === 'custom1' && (
|
||||
@@ -834,6 +835,7 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
||||
selection={custom1OverlaySelection}
|
||||
onSave={sel => handleSaveCustomOverlaySelection('custom1', sel)}
|
||||
onCancel={() => setCustomOverlaySettingsSlot(null)}
|
||||
smaParams={indicators.find(i => i.type === 'SMA')?.params}
|
||||
/>
|
||||
)}
|
||||
{isSingleLoadingMore && (
|
||||
|
||||
@@ -17,6 +17,8 @@ interface ChartCustomOverlaySettingsModalProps {
|
||||
selection: ChartCustomOverlaySelection;
|
||||
onSave: (selection: ChartCustomOverlaySelection) => void;
|
||||
onCancel: () => void;
|
||||
/** SMA 보조지표 params — MA5일 등 동적 라벨 */
|
||||
smaParams?: Record<string, number | string | boolean>;
|
||||
}
|
||||
|
||||
const SECTION_META: Array<{
|
||||
@@ -56,6 +58,7 @@ const ChartCustomOverlaySettingsModal: React.FC<ChartCustomOverlaySettingsModalP
|
||||
selection,
|
||||
onSave,
|
||||
onCancel,
|
||||
smaParams,
|
||||
}) => {
|
||||
const draftRef = useRef(cloneChartCustomOverlaySelection(selection));
|
||||
const [, bump] = useState(0);
|
||||
@@ -110,7 +113,7 @@ const ChartCustomOverlaySettingsModal: React.FC<ChartCustomOverlaySettingsModalP
|
||||
return (
|
||||
<GridToggle
|
||||
key={pid}
|
||||
label={smaMaLabel(i)}
|
||||
label={smaMaLabel(i, smaParams)}
|
||||
checked={draft.smaPlots[pid] !== false}
|
||||
onChange={v => patch(d => { d.smaPlots[pid] = v; })}
|
||||
/>
|
||||
|
||||
@@ -1120,6 +1120,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
selection={customOverlaySelection}
|
||||
onSave={sel => handleSaveCustomOverlaySelection('custom', sel)}
|
||||
onCancel={() => setCustomOverlaySettingsSlot(null)}
|
||||
smaParams={indicators.find(i => i.type === 'SMA')?.params}
|
||||
/>
|
||||
)}
|
||||
{customOverlaySettingsSlot === 'custom1' && !compactMode && (
|
||||
@@ -1128,6 +1129,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
selection={custom1OverlaySelection}
|
||||
onSave={sel => handleSaveCustomOverlaySelection('custom1', sel)}
|
||||
onCancel={() => setCustomOverlaySettingsSlot(null)}
|
||||
smaParams={indicators.find(i => i.type === 'SMA')?.params}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import type { IndicatorConfig, Timeframe } from '../types';
|
||||
import { getIndicatorDef } from '../utils/indicatorRegistry';
|
||||
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
||||
import { getPlotLabel } from '../utils/indicatorLabels';
|
||||
import { getNumericParamSpec } from '../utils/indicatorParamSpec';
|
||||
import NumericParamInput from './NumericParamInput';
|
||||
import SmaPeriodInput from './SmaPeriodInput';
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
SMA_MA_COUNT,
|
||||
smaPeriodKey,
|
||||
smaPlotId,
|
||||
formatSmaMaDisplayLabel,
|
||||
} from '../utils/smaConfig';
|
||||
import PlotLineStylePicker from './PlotLineStylePicker';
|
||||
import {
|
||||
@@ -188,8 +188,14 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
||||
}, [plotVis, patch]);
|
||||
|
||||
const handleSmaPeriod = useCallback((maIndex: number, v: number) => {
|
||||
patch({ params: { ...params, [smaPeriodKey(maIndex)]: v } });
|
||||
}, [params, patch]);
|
||||
const nextParams = { ...params, [smaPeriodKey(maIndex)]: v };
|
||||
const nextPlots = plots.map((pl, i) =>
|
||||
i === maIndex - 1
|
||||
? { ...pl, title: formatSmaMaDisplayLabel(nextParams, maIndex) }
|
||||
: pl,
|
||||
);
|
||||
patch({ params: nextParams, plots: nextPlots });
|
||||
}, [params, plots, patch]);
|
||||
|
||||
const handleSmaToggle = useCallback((plotIndex: number, enabled: boolean) => {
|
||||
patch({ plotVis: { ...plotVis, [smaPlotId(plotIndex)]: enabled } });
|
||||
@@ -254,7 +260,9 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
<label className="ism-label ism-sma-ma-label">{getPlotLabel(`MA${maNum}`)}</label>
|
||||
<label className="ism-label ism-sma-ma-label">
|
||||
{formatSmaMaDisplayLabel(params, maNum)}
|
||||
</label>
|
||||
<div
|
||||
className="ism-control ism-sma-ma-control"
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
@@ -268,7 +276,7 @@ const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
||||
{plot && !inputsOnly && (
|
||||
<PlotLineStylePicker
|
||||
disabled={!enabled}
|
||||
title={`MA${maNum}`}
|
||||
title={formatSmaMaDisplayLabel(params, maNum)}
|
||||
value={{
|
||||
color: plot.color,
|
||||
lineWidth: plot.lineWidth,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ChartManager } from '../utils/ChartManager';
|
||||
import { getIndicatorDef, getIndicatorChartTitle } from '../utils/indicatorRegistry';
|
||||
import { getGlobalParamKeys, getPlotParamKeys } from '../utils/indicatorSettingsLayout';
|
||||
import { formatBbLegendLabel } from '../utils/bollingerConfig';
|
||||
import { formatSmaMaDisplayLabel, SMA_MA_COUNT, smaPlotId } from '../utils/smaConfig';
|
||||
|
||||
export interface PaneItem {
|
||||
id: string;
|
||||
@@ -23,6 +24,17 @@ function makeLabel(config: IndicatorConfig): string {
|
||||
if (config.type === 'BollingerBands') {
|
||||
return formatBbLegendLabel(config.params ?? {});
|
||||
}
|
||||
if (config.type === 'SMA') {
|
||||
const params = (config.params ?? {}) as Record<string, number | string | boolean>;
|
||||
const vis = config.plotVisibility ?? {};
|
||||
const labels: string[] = [];
|
||||
for (let i = 0; i < SMA_MA_COUNT; i++) {
|
||||
const pid = smaPlotId(i);
|
||||
if (vis[pid] === false) continue;
|
||||
labels.push(formatSmaMaDisplayLabel(params, i + 1));
|
||||
}
|
||||
return labels.length ? labels.join(' · ') : getIndicatorChartTitle('SMA');
|
||||
}
|
||||
const name = getIndicatorChartTitle(config.type);
|
||||
const def = getIndicatorDef(config.type);
|
||||
const params = (config.params ?? def?.defaultParams ?? {}) as Record<string, number | string | boolean>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SMA_MA_COUNT, smaPlotId } from './smaConfig';
|
||||
import { SMA_MA_COUNT, smaPlotId, formatSmaMaDisplayLabel } from './smaConfig';
|
||||
|
||||
/** Custom / Custom1 — 동일 기능, 설정만 분리 (세션만, DB 미저장) */
|
||||
export type ChartCustomOverlaySlotId = 'custom' | 'custom1';
|
||||
@@ -70,8 +70,11 @@ export function cloneChartCustomOverlaySelection(
|
||||
};
|
||||
}
|
||||
|
||||
export function smaMaLabel(plotIndex: number): string {
|
||||
return `MA${plotIndex + 1}`;
|
||||
export function smaMaLabel(
|
||||
plotIndex: number,
|
||||
params?: Record<string, number | string | boolean>,
|
||||
): string {
|
||||
return formatSmaMaDisplayLabel(params, plotIndex + 1);
|
||||
}
|
||||
|
||||
export const ICHIMOKU_CUSTOM_ROWS: Array<{
|
||||
|
||||
@@ -231,6 +231,7 @@ export function getParamLabel(key: string, indicatorType?: string): string {
|
||||
}
|
||||
|
||||
export function getPlotLabel(title: string): string {
|
||||
if (/^MA\d+일$/.test(title) || /^EMA\d+일$/.test(title)) return title;
|
||||
const pair = PLOT_LABELS[title];
|
||||
if (pair) return bilingual(pair);
|
||||
return bilingual({ ko: title, en: title });
|
||||
|
||||
@@ -54,7 +54,7 @@ export const MAIN_INDICATOR_LABELS: Record<string, { ko: string; en: string }> =
|
||||
Psychological: { ko: '심리도', en: 'Psychological Line' },
|
||||
NewPsychological: { ko: '신심리도', en: 'New Psychological Line' },
|
||||
InvestPsychological: { ko: '투자심리도', en: 'Invest Psychological Line' },
|
||||
SMA: { ko: '단순 이동평균 (MA1~MA11)', en: 'Simple Moving Average' },
|
||||
SMA: { ko: '단순 이동평균', en: 'Simple Moving Average' },
|
||||
};
|
||||
|
||||
export function isMainIndicatorType(type: string): boolean {
|
||||
|
||||
@@ -268,7 +268,7 @@ export interface IndicatorDef {
|
||||
export const INDICATOR_REGISTRY: IndicatorDef[] = [
|
||||
|
||||
// ── Moving Averages ──────────────────────────────────────────────────────
|
||||
{ type:'SMA', name:'Simple Moving Average', koreanName:'단순이동평균', shortName:'SMA', category:'Moving Averages', overlay:true, defaultParams:createDefaultSmaParams(), plots:createDefaultSmaPlots(), description:'단순 이동평균 (MA1~MA11)' },
|
||||
{ type:'SMA', name:'Simple Moving Average', koreanName:'단순이동평균', shortName:'SMA', category:'Moving Averages', overlay:true, defaultParams:createDefaultSmaParams(), plots:createDefaultSmaPlots(), description:'단순 이동평균 (MA{기간}일 × 11)' },
|
||||
{ type:'EMA', name:'Exponential Moving Average', koreanName:'지수이동평균', shortName:'EMA', category:'Moving Averages', overlay:true, defaultParams:{length:21, src:'close'}, plots:[{id:'plot0',title:'EMA', color:'#FF6D00',type:'line',lineWidth:2}], description:'지수 이동평균' },
|
||||
{ type:'WMA', name:'Weighted Moving Average', koreanName:'가중이동평균', shortName:'WMA', category:'Moving Averages', overlay:true, defaultParams:{length:20, src:'close'}, plots:[{id:'plot0',title:'WMA', color:'#00BCD4',type:'line',lineWidth:2}], description:'가중 이동평균' },
|
||||
{ type:'HMA', name:'Hull Moving Average', koreanName:'헐이동평균', shortName:'HMA', category:'Moving Averages', overlay:true, defaultParams:{length:16, src:'close'}, plots:[{id:'plot0',title:'HMA', color:'#4CAF50',type:'line',lineWidth:2}], description:'헐 이동평균 (래그 최소)' },
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* - MA3 → period3 (예: 20일)
|
||||
* - MA20 → 레거시: 숫자>11 이면 기간 20일 그대로
|
||||
*/
|
||||
import { SMA_DEFAULT_PERIODS, SMA_MA_COUNT } from './smaConfig';
|
||||
import { SMA_DEFAULT_PERIODS, SMA_MA_COUNT, formatSmaPeriodDisplayLabel } from './smaConfig';
|
||||
|
||||
export const MA_DSL_SLOT_MAX = SMA_MA_COUNT;
|
||||
|
||||
@@ -45,7 +45,14 @@ export function resolveMaDslFieldPeriod(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** UI·해석용 라벨 — MA5(60일) */
|
||||
/** EMA 설정 기간 → EMA5일 */
|
||||
export function formatEmaPeriodDisplayLabel(period: number | string | boolean | undefined): string {
|
||||
const raw = typeof period === 'number' ? period : Number(period);
|
||||
const p = Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 1;
|
||||
return `EMA${p}일`;
|
||||
}
|
||||
|
||||
/** UI·해석용 라벨 — MA20일 */
|
||||
export function formatMaDslFieldLabel(
|
||||
field: string | undefined,
|
||||
smaParams?: Record<string, number | string | boolean>,
|
||||
@@ -53,18 +60,8 @@ export function formatMaDslFieldLabel(
|
||||
if (!field) return '';
|
||||
const period = resolveMaDslFieldPeriod(field, smaParams);
|
||||
if (period == null) return field;
|
||||
const ma = /^MA(\d+)$/.exec(field);
|
||||
if (ma && !field.startsWith('MACD')) {
|
||||
const n = parseInt(ma[1], 10);
|
||||
if (n >= 1 && n <= MA_DSL_SLOT_MAX) return `MA${n}(${period}일)`;
|
||||
return `MA(${period}일)`;
|
||||
}
|
||||
const ema = /^EMA(\d+)$/.exec(field);
|
||||
if (ema) {
|
||||
const n = parseInt(ema[1], 10);
|
||||
if (n >= 1 && n <= MA_DSL_SLOT_MAX) return `EMA${n}(${period}일)`;
|
||||
return `EMA(${period}일)`;
|
||||
}
|
||||
if (field.startsWith('EMA')) return formatEmaPeriodDisplayLabel(period);
|
||||
if (field.startsWith('MA') && !field.startsWith('MACD')) return formatSmaPeriodDisplayLabel(period);
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -74,7 +71,7 @@ export function buildMaFieldOptions(
|
||||
return Array.from({ length: SMA_MA_COUNT }, (_, i) => {
|
||||
const slot = i + 1;
|
||||
const p = maLines[i] ?? SMA_DEFAULT_PERIODS[i];
|
||||
return { value: `MA${slot}`, label: `MA${slot}(${p}일)` };
|
||||
return { value: `MA${slot}`, label: formatSmaPeriodDisplayLabel(p) };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,7 +83,7 @@ export function buildEmaFieldOptions(
|
||||
return Array.from({ length: n }, (_, i) => {
|
||||
const slot = i + 1;
|
||||
const p = maLines[i] ?? SMA_DEFAULT_PERIODS[i];
|
||||
return { value: `EMA${slot}`, label: `EMA${slot}(${p}일)` };
|
||||
return { value: `EMA${slot}`, label: formatEmaPeriodDisplayLabel(p) };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,14 +99,14 @@ export function appendLegacyMaFieldOption(
|
||||
if (ma && !field.startsWith('MACD')) {
|
||||
const n = parseInt(ma[1], 10);
|
||||
if (n > MA_DSL_SLOT_MAX) {
|
||||
return [...opts, { value: field, label: `MA(${period}일)` }];
|
||||
return [...opts, { value: field, label: formatSmaPeriodDisplayLabel(period) }];
|
||||
}
|
||||
}
|
||||
const ema = /^EMA(\d+)$/.exec(field);
|
||||
if (ema) {
|
||||
const n = parseInt(ema[1], 10);
|
||||
if (n > MA_DSL_SLOT_MAX) {
|
||||
return [...opts, { value: field, label: `EMA(${period}일)` }];
|
||||
return [...opts, { value: field, label: formatEmaPeriodDisplayLabel(period) }];
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
|
||||
@@ -40,10 +40,49 @@ export function smaPeriodKey(maIndex: number): string {
|
||||
return `period${maIndex}`;
|
||||
}
|
||||
|
||||
/** 설정 기간 → 표시명 MA5일 (보조지표·차트·전략 공통) */
|
||||
export function formatSmaPeriodDisplayLabel(
|
||||
period: number | string | boolean | undefined,
|
||||
fallback = SMA_DEFAULT_PERIODS[0],
|
||||
): string {
|
||||
const raw = typeof period === 'number' ? period : Number(period);
|
||||
const p = Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
|
||||
return `MA${p}일`;
|
||||
}
|
||||
|
||||
export function resolveSmaMaPeriod(
|
||||
params: Record<string, number | string | boolean> | undefined,
|
||||
maIndex: number,
|
||||
): number {
|
||||
const slot = Math.max(1, Math.min(SMA_MA_COUNT, Math.floor(maIndex)));
|
||||
const raw = params?.[smaPeriodKey(slot)];
|
||||
if (typeof raw === 'number' && raw > 0) return Math.floor(raw);
|
||||
return SMA_DEFAULT_PERIODS[slot - 1];
|
||||
}
|
||||
|
||||
/** MA 슬롯(1~11) + params → MA5일 */
|
||||
export function formatSmaMaDisplayLabel(
|
||||
params: Record<string, number | string | boolean> | undefined,
|
||||
maIndex: number,
|
||||
): string {
|
||||
return formatSmaPeriodDisplayLabel(resolveSmaMaPeriod(params, maIndex));
|
||||
}
|
||||
|
||||
/** params 변경 시 plot.title 을 MA{period}일 로 동기화 */
|
||||
export function syncSmaPlotTitles(
|
||||
params: Record<string, number | string | boolean>,
|
||||
plots: PlotDef[],
|
||||
): PlotDef[] {
|
||||
return plots.map((plot, i) => ({
|
||||
...plot,
|
||||
title: formatSmaMaDisplayLabel(params, i + 1),
|
||||
}));
|
||||
}
|
||||
|
||||
export function createDefaultSmaPlots(): PlotDef[] {
|
||||
return SMA_DEFAULT_PERIODS.map((_, i) => ({
|
||||
return SMA_DEFAULT_PERIODS.map((period, i) => ({
|
||||
id: smaPlotId(i),
|
||||
title: `MA${i + 1}`,
|
||||
title: formatSmaPeriodDisplayLabel(period),
|
||||
color: SMA_DEFAULT_COLORS[i],
|
||||
type: 'line',
|
||||
lineWidth: 2,
|
||||
@@ -114,7 +153,7 @@ export function normalizeSmaConfig(config: IndicatorConfig): IndicatorConfig {
|
||||
const plots: PlotDef[] = defaultPlots.map((def, i) => {
|
||||
const saved = savedById.get(def.id) ?? config.plots?.[i];
|
||||
return saved
|
||||
? { ...def, ...saved, id: def.id, title: `MA${i + 1}`, type: 'line' as const }
|
||||
? { ...def, ...saved, id: def.id, title: formatSmaMaDisplayLabel(params, i + 1), type: 'line' as const }
|
||||
: def;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user