전략편집기 조건 목록 수정
This commit is contained in:
@@ -26,6 +26,11 @@ export interface IndicatorSettingsListRowProps {
|
|||||||
hideChartToggle?: boolean;
|
hideChartToggle?: boolean;
|
||||||
/** 출력·표시(가격축 라벨·타임프레임) 숨김 */
|
/** 출력·표시(가격축 라벨·타임프레임) 숨김 */
|
||||||
hideOutputFooter?: boolean;
|
hideOutputFooter?: boolean;
|
||||||
|
/** 오버레이 지표(SMA·볼린저·일목) 전체 on/off — 오버레이 뱃지·기본값 버튼 대신 표시 */
|
||||||
|
overlayMasterToggle?: {
|
||||||
|
visible: boolean;
|
||||||
|
onChange: (visible: boolean) => void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
||||||
@@ -47,6 +52,7 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
|||||||
onDrop,
|
onDrop,
|
||||||
hideChartToggle = false,
|
hideChartToggle = false,
|
||||||
hideOutputFooter = false,
|
hideOutputFooter = false,
|
||||||
|
overlayMasterToggle,
|
||||||
}) => {
|
}) => {
|
||||||
const labels = getIndicatorListLabels(type);
|
const labels = getIndicatorListLabels(type);
|
||||||
const def = getIndicatorDef(type);
|
const def = getIndicatorDef(type);
|
||||||
@@ -92,9 +98,9 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
|||||||
<span className={enClass}>{labels.en}</span>
|
<span className={enClass}>{labels.en}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={badgesClass}>
|
<div className={badgesClass}>
|
||||||
{def?.overlay && <span className="ind-panel-badge">오버레이</span>}
|
{!overlayMasterToggle && def?.overlay && <span className="ind-panel-badge">오버레이</span>}
|
||||||
{def?.returnsMarkers && <span className="ind-panel-badge">마커</span>}
|
{def?.returnsMarkers && <span className="ind-panel-badge">마커</span>}
|
||||||
{!hideChartToggle && (
|
{!overlayMasterToggle && !hideChartToggle && (
|
||||||
enabled ? (
|
enabled ? (
|
||||||
<span className={onBadgeClass}>차트 적용</span>
|
<span className={onBadgeClass}>차트 적용</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -102,7 +108,21 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!hideChartToggle && (
|
{overlayMasterToggle ? (
|
||||||
|
<label
|
||||||
|
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
||||||
|
title={overlayMasterToggle.visible ? '차트에서 숨기기' : '차트에 표시'}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={overlayMasterToggle.visible}
|
||||||
|
onChange={e => overlayMasterToggle.onChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="ism-toggle-slider" />
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
!hideChartToggle && (
|
||||||
<label
|
<label
|
||||||
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
className={`ibsm-toggle${variant === 'settings' ? ' stg-ind-toggle' : ''}`}
|
||||||
title={enabled ? '차트에서 제거' : '차트에 추가'}
|
title={enabled ? '차트에서 제거' : '차트에 추가'}
|
||||||
@@ -115,8 +135,9 @@ const IndicatorSettingsListRow: React.FC<IndicatorSettingsListRowProps> = ({
|
|||||||
/>
|
/>
|
||||||
<span className="ism-toggle-slider" />
|
<span className="ism-toggle-slider" />
|
||||||
</label>
|
</label>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{variant === 'settings' && onRowDefaults && (
|
{variant === 'settings' && onRowDefaults && !overlayMasterToggle && (
|
||||||
<button type="button" className="stg-ind-card-defaults" onClick={onRowDefaults}>
|
<button type="button" className="stg-ind-card-defaults" onClick={onRowDefaults}>
|
||||||
기본값
|
기본값
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ import StrategyEvaluationChartSettingsTab from './strategyEvaluation/StrategyEva
|
|||||||
import PaletteDragOverlay from './strategyEditor/PaletteDragOverlay';
|
import PaletteDragOverlay from './strategyEditor/PaletteDragOverlay';
|
||||||
import { needsPointerPaletteDrag } from '../utils/paletteDragSession';
|
import { needsPointerPaletteDrag } from '../utils/paletteDragSession';
|
||||||
import { useStrategyEvaluationEditor } from '../hooks/useStrategyEvaluationEditor';
|
import { useStrategyEvaluationEditor } from '../hooks/useStrategyEvaluationEditor';
|
||||||
|
import {
|
||||||
|
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||||
|
type ChartOverlayToggleKey,
|
||||||
|
type ChartOverlayVisibility,
|
||||||
|
} from '../utils/chartOverlayVisibility';
|
||||||
import {
|
import {
|
||||||
buildStrategyEvaluationAiVerifyContext,
|
buildStrategyEvaluationAiVerifyContext,
|
||||||
requestStrategyEvaluationAiVerify,
|
requestStrategyEvaluationAiVerify,
|
||||||
@@ -112,6 +117,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
const [reportModel, setReportModel] = useState<BacktestAnalysisReportModel | null>(null);
|
const [reportModel, setReportModel] = useState<BacktestAnalysisReportModel | null>(null);
|
||||||
const [reportLoading, setReportLoading] = useState(false);
|
const [reportLoading, setReportLoading] = useState(false);
|
||||||
const [rightTab, setRightTab] = useState<RightTab>('signal');
|
const [rightTab, setRightTab] = useState<RightTab>('signal');
|
||||||
|
const [overlayVisibility, setOverlayVisibility] = useState<ChartOverlayVisibility>(
|
||||||
|
() => ({ ...DEFAULT_CHART_OVERLAY_VISIBILITY }),
|
||||||
|
);
|
||||||
const [aiVerifyUnread, setAiVerifyUnread] = useState(false);
|
const [aiVerifyUnread, setAiVerifyUnread] = useState(false);
|
||||||
const [aiVerifyRunning, setAiVerifyRunning] = useState(false);
|
const [aiVerifyRunning, setAiVerifyRunning] = useState(false);
|
||||||
const [aiVerifyError, setAiVerifyError] = useState<string | null>(null);
|
const [aiVerifyError, setAiVerifyError] = useState<string | null>(null);
|
||||||
@@ -143,6 +151,10 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
[baseGetParams, appliedIndicatorParams],
|
[baseGetParams, appliedIndicatorParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSetOverlayVisibility = useCallback((key: ChartOverlayToggleKey, visible: boolean) => {
|
||||||
|
setOverlayVisibility(prev => ({ ...prev, [key]: visible }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleStrategyEditorSaved = useCallback((saved: StrategyDto, meta?: { created?: boolean }) => {
|
const handleStrategyEditorSaved = useCallback((saved: StrategyDto, meta?: { created?: boolean }) => {
|
||||||
const hydrated = hydrateStrategyDto(saved);
|
const hydrated = hydrateStrategyDto(saved);
|
||||||
setSelectedStrategy(hydrated);
|
setSelectedStrategy(hydrated);
|
||||||
@@ -344,6 +356,13 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
|
|
||||||
const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null;
|
const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bars.length === 0) return;
|
||||||
|
if (selectedBarIndex >= bars.length) {
|
||||||
|
setSelectedBarIndex(bars.length - 1);
|
||||||
|
}
|
||||||
|
}, [bars.length, selectedBarIndex]);
|
||||||
|
|
||||||
const barSignalHighlight = useMemo(
|
const barSignalHighlight = useMemo(
|
||||||
() => resolveBarSignalHighlight(backtestSignals, selectedBarTimeSec, selectedBarIndex),
|
() => resolveBarSignalHighlight(backtestSignals, selectedBarTimeSec, selectedBarIndex),
|
||||||
[backtestSignals, selectedBarTimeSec, selectedBarIndex],
|
[backtestSignals, selectedBarTimeSec, selectedBarIndex],
|
||||||
@@ -815,6 +834,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
marketLoading={marketFeed.loading}
|
marketLoading={marketFeed.loading}
|
||||||
refreshMarketTickers={marketFeed.refreshAllTickers}
|
refreshMarketTickers={marketFeed.refreshAllTickers}
|
||||||
onMarketSearchOpenChange={setMarketSearchOpen}
|
onMarketSearchOpenChange={setMarketSearchOpen}
|
||||||
|
overlayVisibility={overlayVisibility}
|
||||||
|
onSetOverlayVisibility={handleSetOverlayVisibility}
|
||||||
/>
|
/>
|
||||||
<StrategyEvaluationStrategyPanel
|
<StrategyEvaluationStrategyPanel
|
||||||
strategy={selectedStrategy}
|
strategy={selectedStrategy}
|
||||||
@@ -980,6 +1001,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
saveVisual={saveVisual}
|
saveVisual={saveVisual}
|
||||||
settingsRevision={settingsRevision}
|
settingsRevision={settingsRevision}
|
||||||
onPersisted={handleChartSettingsPersisted}
|
onPersisted={handleChartSettingsPersisted}
|
||||||
|
overlayVisibility={overlayVisibility}
|
||||||
|
onSetOverlayVisibility={handleSetOverlayVisibility}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
DEFAULT_CHART_OVERLAY_VISIBILITY,
|
||||||
effectiveConfigForOverlay,
|
effectiveConfigForOverlay,
|
||||||
listChartOverlayToggleItems,
|
listChartOverlayToggleItems,
|
||||||
|
CHART_OVERLAY_TOGGLE_ORDER,
|
||||||
type ChartOverlayToggleKey,
|
type ChartOverlayToggleKey,
|
||||||
type ChartOverlayVisibility,
|
type ChartOverlayVisibility,
|
||||||
} from '../../utils/chartOverlayVisibility';
|
} from '../../utils/chartOverlayVisibility';
|
||||||
@@ -49,6 +50,7 @@ import {
|
|||||||
parseBacktestRunTimeframeChoice,
|
parseBacktestRunTimeframeChoice,
|
||||||
type BacktestRunTimeframeChoice,
|
type BacktestRunTimeframeChoice,
|
||||||
} from '../../utils/backtestRunTimeframe';
|
} from '../../utils/backtestRunTimeframe';
|
||||||
|
import { timeframeBarSeconds } from '../../utils/backtestUiUtils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
market: string;
|
market: string;
|
||||||
@@ -100,6 +102,9 @@ interface Props {
|
|||||||
marketLoading?: boolean;
|
marketLoading?: boolean;
|
||||||
refreshMarketTickers?: () => Promise<void>;
|
refreshMarketTickers?: () => Promise<void>;
|
||||||
onMarketSearchOpenChange?: (open: boolean) => void;
|
onMarketSearchOpenChange?: (open: boolean) => void;
|
||||||
|
/** 캔들 오버레이(SMA·볼린저·일목·캔들) 표시 — 미전달 시 내부 state */
|
||||||
|
overlayVisibility?: ChartOverlayVisibility;
|
||||||
|
onSetOverlayVisibility?: (key: ChartOverlayToggleKey, visible: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
|
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
|
||||||
@@ -144,6 +149,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
marketLoading,
|
marketLoading,
|
||||||
refreshMarketTickers,
|
refreshMarketTickers,
|
||||||
onMarketSearchOpenChange,
|
onMarketSearchOpenChange,
|
||||||
|
overlayVisibility: overlayVisibilityProp,
|
||||||
|
onSetOverlayVisibility,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams: baseGetParams, getVisualConfig: baseGetVisual, settingsRevision } = useIndicatorSettings();
|
const { getParams: baseGetParams, getVisualConfig: baseGetVisual, settingsRevision } = useIndicatorSettings();
|
||||||
const getParams = getParamsOverride ?? baseGetParams;
|
const getParams = getParamsOverride ?? baseGetParams;
|
||||||
@@ -163,13 +170,15 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
const [magnifierActive, setMagnifierActive] = useState(false);
|
const [magnifierActive, setMagnifierActive] = useState(false);
|
||||||
const marketBtnRef = useRef<HTMLButtonElement>(null);
|
const marketBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
const managerRef = useRef<ChartManager | null>(null);
|
const managerRef = useRef<ChartManager | null>(null);
|
||||||
const [overlayVisibility, setOverlayVisibility] = useState<ChartOverlayVisibility>(
|
const [internalOverlayVisibility, setInternalOverlayVisibility] = useState<ChartOverlayVisibility>(
|
||||||
() => ({ ...DEFAULT_CHART_OVERLAY_VISIBILITY }),
|
() => ({ ...DEFAULT_CHART_OVERLAY_VISIBILITY }),
|
||||||
);
|
);
|
||||||
|
const overlayVisibility = overlayVisibilityProp ?? internalOverlayVisibility;
|
||||||
const overlayVisibilityRef = useRef(overlayVisibility);
|
const overlayVisibilityRef = useRef(overlayVisibility);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
overlayVisibilityRef.current = overlayVisibility;
|
overlayVisibilityRef.current = overlayVisibility;
|
||||||
}, [overlayVisibility]);
|
}, [overlayVisibility]);
|
||||||
|
|
||||||
const backtestGenRef = useRef(0);
|
const backtestGenRef = useRef(0);
|
||||||
const evaluationBarCountRef = useRef(BACKTEST_DISPLAY_BAR_COUNT);
|
const evaluationBarCountRef = useRef(BACKTEST_DISPLAY_BAR_COUNT);
|
||||||
const signalsRef = useRef(signals);
|
const signalsRef = useRef(signals);
|
||||||
@@ -196,12 +205,13 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
[overlayVisibility],
|
[overlayVisibility],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
const applyOverlayToManager = useCallback((
|
||||||
setOverlayVisibility(prev => {
|
key: ChartOverlayToggleKey,
|
||||||
const next = { ...prev, [key]: !prev[key] };
|
fullVisibility: ChartOverlayVisibility,
|
||||||
|
) => {
|
||||||
const mgr = managerRef.current;
|
const mgr = managerRef.current;
|
||||||
mgr?.setChartOverlayGroupVisible(key, next[key]);
|
if (!mgr) return;
|
||||||
if (mgr) {
|
mgr.setChartOverlayVisibility(fullVisibility);
|
||||||
for (const info of mgr.getIndicatorPaneInfo()) {
|
for (const info of mgr.getIndicatorPaneInfo()) {
|
||||||
if (chartOverlayKeyForType(info.type) !== key) continue;
|
if (chartOverlayKeyForType(info.type) !== key) continue;
|
||||||
const reactInd = indicatorsRef.current.find(i => i.id === info.id);
|
const reactInd = indicatorsRef.current.find(i => i.id === info.id);
|
||||||
@@ -209,11 +219,35 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
? { ...info.config, ...reactInd, id: info.id, type: info.type }
|
? { ...info.config, ...reactInd, id: info.id, type: info.type }
|
||||||
: info.config);
|
: info.config);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
||||||
|
const nextVisible = !overlayVisibilityRef.current[key];
|
||||||
|
const next = { ...overlayVisibilityRef.current, [key]: nextVisible };
|
||||||
|
if (onSetOverlayVisibility) {
|
||||||
|
onSetOverlayVisibility(key, nextVisible);
|
||||||
|
} else {
|
||||||
|
setInternalOverlayVisibility(next);
|
||||||
|
applyOverlayToManager(key, next);
|
||||||
|
}
|
||||||
|
}, [onSetOverlayVisibility, applyOverlayToManager]);
|
||||||
|
|
||||||
|
const prevOverlayVisibilityRef = useRef(overlayVisibility);
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = prevOverlayVisibilityRef.current;
|
||||||
|
prevOverlayVisibilityRef.current = overlayVisibility;
|
||||||
|
const mgr = managerRef.current;
|
||||||
|
if (!mgr) return;
|
||||||
|
mgr.setChartOverlayVisibility(overlayVisibility);
|
||||||
|
if (onSetOverlayVisibility) {
|
||||||
|
for (const key of CHART_OVERLAY_TOGGLE_ORDER) {
|
||||||
|
if (prev[key] !== overlayVisibility[key]) {
|
||||||
|
applyOverlayToManager(key, overlayVisibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [overlayVisibility, onSetOverlayVisibility, applyOverlayToManager]);
|
||||||
|
|
||||||
const [customOverlaySelection, setCustomOverlaySelection] = useState<ChartCustomOverlaySelection>(
|
const [customOverlaySelection, setCustomOverlaySelection] = useState<ChartCustomOverlaySelection>(
|
||||||
() => createDefaultChartCustomOverlaySelection(),
|
() => createDefaultChartCustomOverlaySelection(),
|
||||||
);
|
);
|
||||||
@@ -435,12 +469,25 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
}, [market, timeframe, strategy?.id, paramsRevision, onBarsLoaded, onSelectedBarIndexChange]);
|
}, [market, timeframe, strategy?.id, paramsRevision, onBarsLoaded, onSelectedBarIndexChange]);
|
||||||
|
|
||||||
const scrollToBar = useCallback((barIdx: number) => {
|
const scrollToBar = useCallback((barIdx: number) => {
|
||||||
const mgr = chartMgr;
|
const mgr = managerRef.current;
|
||||||
if (!mgr || barIdx < 0 || barIdx >= bars.length) return;
|
if (!mgr?.hasMainSeries() || barIdx < 0 || barIdx >= bars.length) return;
|
||||||
const bar = bars[barIdx];
|
const bar = bars[barIdx];
|
||||||
const barSec = Math.max(60, 180);
|
const barSec = timeframeBarSeconds(timeframe);
|
||||||
mgr.applyVisibleTimeRange(bar.time - barSec * 24, bar.time + barSec * 8);
|
const pad = Math.max(barSec * 48, 3600);
|
||||||
}, [chartMgr, bars]);
|
mgr.applyVisibleTimeRange(bar.time - pad, bar.time + Math.max(barSec * 8, 600));
|
||||||
|
}, [bars, timeframe]);
|
||||||
|
|
||||||
|
const prevSelectedBarIndexRef = useRef(selectedBarIndex);
|
||||||
|
useEffect(() => {
|
||||||
|
prevSelectedBarIndexRef.current = -1;
|
||||||
|
}, [market, timeframe, strategy?.id, paramsRevision]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bars.length === 0) return;
|
||||||
|
if (prevSelectedBarIndexRef.current === selectedBarIndex) return;
|
||||||
|
prevSelectedBarIndexRef.current = selectedBarIndex;
|
||||||
|
scrollToBar(selectedBarIndex);
|
||||||
|
}, [selectedBarIndex, bars.length, scrollToBar]);
|
||||||
|
|
||||||
const onManagerReady = useCallback((mgr: ChartManager) => {
|
const onManagerReady = useCallback((mgr: ChartManager) => {
|
||||||
managerRef.current = mgr;
|
managerRef.current = mgr;
|
||||||
@@ -467,15 +514,14 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
const stepBar = useCallback((delta: number) => {
|
const stepBar = useCallback((delta: number) => {
|
||||||
if (bars.length === 0) return;
|
if (bars.length === 0) return;
|
||||||
const next = Math.min(bars.length - 1, Math.max(0, selectedBarIndex + delta));
|
const next = Math.min(bars.length - 1, Math.max(0, selectedBarIndex + delta));
|
||||||
|
if (next === selectedBarIndex) return;
|
||||||
onSelectedBarIndexChange(next);
|
onSelectedBarIndexChange(next);
|
||||||
}, [bars.length, selectedBarIndex, onSelectedBarIndexChange]);
|
}, [bars.length, selectedBarIndex, onSelectedBarIndexChange]);
|
||||||
|
|
||||||
const goLatest = useCallback(() => {
|
const goLatest = useCallback(() => {
|
||||||
if (bars.length === 0) return;
|
if (bars.length === 0) return;
|
||||||
const lastIdx = bars.length - 1;
|
onSelectedBarIndexChange(bars.length - 1);
|
||||||
onSelectedBarIndexChange(lastIdx);
|
}, [bars.length, onSelectedBarIndexChange]);
|
||||||
scrollToBar(lastIdx);
|
|
||||||
}, [bars.length, onSelectedBarIndexChange, scrollToBar]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mgr = chartMgr;
|
const mgr = chartMgr;
|
||||||
|
|||||||
@@ -21,9 +21,15 @@ import {
|
|||||||
import type { IchimokuCloudColors } from '../../utils/ichimokuConfig';
|
import type { IchimokuCloudColors } from '../../utils/ichimokuConfig';
|
||||||
import ChartPriceAxisLabelSettings from './ChartPriceAxisLabelSettings';
|
import ChartPriceAxisLabelSettings from './ChartPriceAxisLabelSettings';
|
||||||
import { useAppSettings, resolveAppDefaults } from '../../hooks/useAppSettings';
|
import { useAppSettings, resolveAppDefaults } from '../../hooks/useAppSettings';
|
||||||
|
import {
|
||||||
|
chartOverlayKeyForType,
|
||||||
|
type ChartOverlayToggleKey,
|
||||||
|
type ChartOverlayVisibility,
|
||||||
|
} from '../../utils/chartOverlayVisibility';
|
||||||
|
|
||||||
const EVAL_CHART_OVERLAY_TYPES = ['SMA', 'BollingerBands', 'IchimokuCloud'] as const;
|
const EVAL_CHART_OVERLAY_TYPES = ['SMA', 'BollingerBands', 'IchimokuCloud'] as const;
|
||||||
const EVAL_CHART_OVERLAY_SET = new Set<string>(EVAL_CHART_OVERLAY_TYPES);
|
const EVAL_CHART_OVERLAY_SET = new Set<string>(EVAL_CHART_OVERLAY_TYPES);
|
||||||
|
const EVAL_OVERLAY_MASTER_TYPES = new Set<string>(EVAL_CHART_OVERLAY_TYPES);
|
||||||
|
|
||||||
function buildEvalChartSettingsTypes(strategy: StrategyDto | null): string[] {
|
function buildEvalChartSettingsTypes(strategy: StrategyDto | null): string[] {
|
||||||
const strategyTypes = strategy ? collectStrategyRegistryTypes(strategy) : [];
|
const strategyTypes = strategy ? collectStrategyRegistryTypes(strategy) : [];
|
||||||
@@ -63,6 +69,9 @@ interface Props {
|
|||||||
settingsRevision?: number;
|
settingsRevision?: number;
|
||||||
/** DB 저장 후 세션 오버라이드 제거·차트 갱신 */
|
/** DB 저장 후 세션 오버라이드 제거·차트 갱신 */
|
||||||
onPersisted?: (type: string) => void;
|
onPersisted?: (type: string) => void;
|
||||||
|
/** 캔들 오버레이(SMA·볼린저·일목) 표시 on/off */
|
||||||
|
overlayVisibility: ChartOverlayVisibility;
|
||||||
|
onSetOverlayVisibility: (key: ChartOverlayToggleKey, visible: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
||||||
@@ -74,6 +83,8 @@ const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
|||||||
saveVisual,
|
saveVisual,
|
||||||
settingsRevision = 0,
|
settingsRevision = 0,
|
||||||
onPersisted,
|
onPersisted,
|
||||||
|
overlayVisibility,
|
||||||
|
onSetOverlayVisibility,
|
||||||
}) => {
|
}) => {
|
||||||
const indicatorTypes = useMemo(
|
const indicatorTypes = useMemo(
|
||||||
() => buildEvalChartSettingsTypes(strategy),
|
() => buildEvalChartSettingsTypes(strategy),
|
||||||
@@ -153,6 +164,8 @@ const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
|||||||
const renderRows = (types: string[]) => types.map(type => {
|
const renderRows = (types: string[]) => types.map(type => {
|
||||||
const cfg = configs[type];
|
const cfg = configs[type];
|
||||||
if (!cfg) return null;
|
if (!cfg) return null;
|
||||||
|
const overlayKey = chartOverlayKeyForType(type);
|
||||||
|
const isOverlayMaster = EVAL_OVERLAY_MASTER_TYPES.has(type) && overlayKey != null;
|
||||||
return (
|
return (
|
||||||
<IndicatorSettingsListRow
|
<IndicatorSettingsListRow
|
||||||
key={type}
|
key={type}
|
||||||
@@ -167,7 +180,11 @@ const StrategyEvaluationChartSettingsTab: React.FC<Props> = ({
|
|||||||
onToggleExpand={() => toggleExpand(type)}
|
onToggleExpand={() => toggleExpand(type)}
|
||||||
onToggleEnabled={() => {}}
|
onToggleEnabled={() => {}}
|
||||||
onChange={schedulePersist}
|
onChange={schedulePersist}
|
||||||
onRowDefaults={() => handleRowDefaults(type)}
|
onRowDefaults={isOverlayMaster ? undefined : () => handleRowDefaults(type)}
|
||||||
|
overlayMasterToggle={isOverlayMaster ? {
|
||||||
|
visible: overlayVisibility[overlayKey!],
|
||||||
|
onChange: visible => onSetOverlayVisibility(overlayKey!, visible),
|
||||||
|
} : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
syncCompositeFields,
|
syncCompositeFields,
|
||||||
type CompositePeriodDef,
|
type CompositePeriodDef,
|
||||||
} from './compositeIndicators';
|
} from './compositeIndicators';
|
||||||
|
import { getStrategyIndicatorDisplayName } from './strategyPaletteStorage';
|
||||||
import {
|
import {
|
||||||
isPriceExtremeIndicator,
|
isPriceExtremeIndicator,
|
||||||
migratePriceExtremeCondition,
|
migratePriceExtremeCondition,
|
||||||
@@ -55,6 +56,7 @@ export const VALUE_FIELD_PREFIX: Record<string, string> = {
|
|||||||
WILLIAMS_R: 'WILLIAMS_R_VALUE',
|
WILLIAMS_R: 'WILLIAMS_R_VALUE',
|
||||||
TRIX: 'TRIX_VALUE',
|
TRIX: 'TRIX_VALUE',
|
||||||
VR: 'VR_VALUE',
|
VR: 'VR_VALUE',
|
||||||
|
BWI: 'BWI_VALUE',
|
||||||
PSYCHOLOGICAL: 'PSY_VALUE',
|
PSYCHOLOGICAL: 'PSY_VALUE',
|
||||||
NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE',
|
NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE',
|
||||||
INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE',
|
INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE',
|
||||||
@@ -447,14 +449,104 @@ export function hasNodeSettings(cond: ConditionDSL): boolean {
|
|||||||
|| !!cond.composite;
|
|| !!cond.composite;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMON_PERIODS = [5, 7, 9, 10, 12, 13, 14, 20, 21, 26, 28, 50, 60, 120];
|
export const COMMON_PERIODS = [5, 7, 9, 10, 12, 13, 14, 20, 21, 26, 28, 50, 60, 120];
|
||||||
|
|
||||||
|
/** 지표별 추가 기간 프리셋 — HWP·UI 스펙 (예: CCI 100·200일) */
|
||||||
|
const INDICATOR_EXTRA_PERIODS: Record<string, number[]> = {
|
||||||
|
CCI: [100, 200],
|
||||||
|
};
|
||||||
|
|
||||||
|
function mergePeriodPresets(...groups: number[][]): number[] {
|
||||||
|
return [...new Set(groups.flat())].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 단일·복합 공통 — 지표별 기간 프리셋 */
|
||||||
|
export function getIndicatorPeriodPresets(
|
||||||
|
indicatorType: string,
|
||||||
|
def: IndicatorPeriodDef,
|
||||||
|
extra: number[] = [],
|
||||||
|
): number[] {
|
||||||
|
const base = getDefaultIndicatorPeriod(indicatorType, def);
|
||||||
|
const indicatorExtras = INDICATOR_EXTRA_PERIODS[indicatorType] ?? [];
|
||||||
|
if (isPriceExtremeIndicator(indicatorType)) {
|
||||||
|
return mergePeriodPresets([base, 5, 9, 10, 20, 55], COMMON_PERIODS, indicatorExtras, extra);
|
||||||
|
}
|
||||||
|
return mergePeriodPresets([base], COMMON_PERIODS, indicatorExtras, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 단일 보조지표 값 라인 라벨 — 예: CCI 1 라인(9일) */
|
||||||
|
export function singleIndicatorLineLabel(indicatorType: string, period: number): string {
|
||||||
|
const name = getStrategyIndicatorDisplayName(indicatorType);
|
||||||
|
return `${name} 1 라인(${period}일)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 단일 보조지표 조건대상1 값·기간 라벨 — 지표별 HWP 표기 */
|
||||||
|
export function singleIndicatorValuePeriodLabel(indicatorType: string, period: number): string {
|
||||||
|
switch (indicatorType) {
|
||||||
|
case 'CCI':
|
||||||
|
return `CCI 기간 ${period}`;
|
||||||
|
case 'RSI':
|
||||||
|
return `RSI 기간 ${period}`;
|
||||||
|
case 'ADX':
|
||||||
|
return `ADX 기간 ${period}`;
|
||||||
|
case 'WILLIAMS_R':
|
||||||
|
return `williams%R 기간${period}`;
|
||||||
|
case 'TRIX':
|
||||||
|
return `TRIX ${period}`;
|
||||||
|
default:
|
||||||
|
return singleIndicatorLineLabel(indicatorType, period);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** HWP — 조건대상1 기본 값 필드 (차트 설정 기간 + 직접입력) */
|
||||||
|
export function buildHwpValueFieldOpt(
|
||||||
|
indicatorType: string,
|
||||||
|
def: IndicatorPeriodDef,
|
||||||
|
cond?: ConditionDSL,
|
||||||
|
): { value: string; label: string } | null {
|
||||||
|
const prefix = VALUE_FIELD_PREFIX[indicatorType];
|
||||||
|
if (!prefix) return null;
|
||||||
|
const mock = cond ? { ...cond, indicatorType } : { indicatorType } as ConditionDSL;
|
||||||
|
const period = getConditionValuePeriod(mock, def);
|
||||||
|
return { value: prefix, label: singleIndicatorValuePeriodLabel(indicatorType, period) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 단일 보조지표 조건대상1 — 기간별 값 라인 드롭다운 (직접입력은 ComboFieldSelect) */
|
||||||
|
export function buildSingleIndicatorPeriodOpts(
|
||||||
|
indicatorType: string,
|
||||||
|
def: IndicatorPeriodDef,
|
||||||
|
cond?: ConditionDSL,
|
||||||
|
): { value: string; label: string }[] {
|
||||||
|
const prefix = VALUE_FIELD_PREFIX[indicatorType];
|
||||||
|
if (!prefix) return [];
|
||||||
|
const mock = cond ? { ...cond, indicatorType } : { indicatorType } as ConditionDSL;
|
||||||
|
const current = getConditionValuePeriod(mock, def);
|
||||||
|
const periods = [...new Set([...getPeriodPresetOptions(indicatorType, def), current])].sort((a, b) => a - b);
|
||||||
|
return periods.map(p => ({
|
||||||
|
value: `${prefix}_${p}`,
|
||||||
|
label: singleIndicatorLineLabel(indicatorType, p),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RSI_VALUE → RSI_VALUE_9 — 옵션·라벨 조회용 */
|
||||||
|
export function resolveStoredValueField(
|
||||||
|
indicatorType: string,
|
||||||
|
field: string | undefined,
|
||||||
|
def: IndicatorPeriodDef,
|
||||||
|
cond?: ConditionDSL,
|
||||||
|
): string | undefined {
|
||||||
|
if (!field) return field;
|
||||||
|
const prefix = VALUE_FIELD_PREFIX[indicatorType];
|
||||||
|
if (prefix && field === prefix) {
|
||||||
|
const mock = cond ? { ...cond, indicatorType, leftField: field } : { indicatorType, leftField: field } as ConditionDSL;
|
||||||
|
const p = getConditionValuePeriod(mock, def);
|
||||||
|
return `${prefix}_${p}`;
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
export function getPeriodPresetOptions(indicatorType: string, def: IndicatorPeriodDef): number[] {
|
export function getPeriodPresetOptions(indicatorType: string, def: IndicatorPeriodDef): number[] {
|
||||||
const base = getDefaultIndicatorPeriod(indicatorType, def);
|
return getIndicatorPeriodPresets(indicatorType, def);
|
||||||
if (isPriceExtremeIndicator(indicatorType)) {
|
|
||||||
return [...new Set([base, 5, 9, 10, 20, 55, ...COMMON_PERIODS])].sort((a, b) => a - b);
|
|
||||||
}
|
|
||||||
return [...new Set([base, ...COMMON_PERIODS])].sort((a, b) => a - b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPeriodSettingsLabel(cond: ConditionDSL): string | null {
|
export function getPeriodSettingsLabel(cond: ConditionDSL): string | null {
|
||||||
@@ -472,7 +564,7 @@ export function getCompositePeriodPresetOptions(
|
|||||||
): number[] {
|
): number[] {
|
||||||
const { short, long } = getCompositeDefaultPeriods(indicatorType, def as CompositePeriodDef);
|
const { short, long } = getCompositeDefaultPeriods(indicatorType, def as CompositePeriodDef);
|
||||||
const base = side === 'left' ? short : long;
|
const base = side === 'left' ? short : long;
|
||||||
return [...new Set([base, short, long, ...COMMON_PERIODS])].sort((a, b) => a - b);
|
return getIndicatorPeriodPresets(indicatorType, def, [base, short, long]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 복합지표 조건대상1/2 드롭다운 — CCI 1 라인(9일) 형식 */
|
/** 복합지표 조건대상1/2 드롭다운 — CCI 1 라인(9일) 형식 */
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ import {
|
|||||||
getCompositeLeftCandleType,
|
getCompositeLeftCandleType,
|
||||||
getCompositeRightCandleType,
|
getCompositeRightCandleType,
|
||||||
ensureDirectThresholdSnapshot,
|
ensureDirectThresholdSnapshot,
|
||||||
|
getConditionValuePeriod,
|
||||||
parseThresholdField,
|
parseThresholdField,
|
||||||
|
resolveStoredValueField,
|
||||||
|
singleIndicatorLineLabel,
|
||||||
|
singleIndicatorValuePeriodLabel,
|
||||||
} from './conditionPeriods';
|
} from './conditionPeriods';
|
||||||
import { formatStrategyCandleLabel } from './strategyStartNodes';
|
import { formatStrategyCandleLabel } from './strategyStartNodes';
|
||||||
import { formatStartCandleLabel } from './strategyStartNodes';
|
import { formatStartCandleLabel } from './strategyStartNodes';
|
||||||
@@ -83,9 +87,23 @@ function fieldLabel(
|
|||||||
if (hit) return hit.label;
|
if (hit) return hit.label;
|
||||||
}
|
}
|
||||||
const opts = getFieldOpts(indicatorType, signalType, def, cond);
|
const opts = getFieldOpts(indicatorType, signalType, def, cond);
|
||||||
const resolved = resolveFieldOptionValue(indicatorType, field);
|
const stored = resolveStoredValueField(indicatorType, field, def, cond);
|
||||||
const hit = opts.find(o => o.value === resolved);
|
const hit = opts.find(o => o.value === stored) ?? opts.find(o => o.value === field);
|
||||||
if (hit && hit.label !== '선택안함') return hit.label;
|
if (hit && hit.label !== '선택안함') return hit.label;
|
||||||
|
const resolved = resolveFieldOptionValue(indicatorType, field);
|
||||||
|
const hitLegacy = opts.find(o => o.value === resolved);
|
||||||
|
if (hitLegacy && hitLegacy.label !== '선택안함') return hitLegacy.label;
|
||||||
|
const m = stored?.match(/_(\d+)$/);
|
||||||
|
if (m) return singleIndicatorValuePeriodLabel(indicatorType, parseInt(m[1], 10));
|
||||||
|
if (cond) {
|
||||||
|
const base = {
|
||||||
|
RSI: 'RSI_VALUE', CCI: 'CCI_VALUE', ADX: 'ADX_VALUE',
|
||||||
|
WILLIAMS_R: 'WILLIAMS_R_VALUE', TRIX: 'TRIX_VALUE',
|
||||||
|
}[indicatorType];
|
||||||
|
if (base && (stored === base || field === base)) {
|
||||||
|
return singleIndicatorValuePeriodLabel(indicatorType, getConditionValuePeriod(cond, def));
|
||||||
|
}
|
||||||
|
}
|
||||||
const thresh = field ? parseThresholdField(field) : null;
|
const thresh = field ? parseThresholdField(field) : null;
|
||||||
if (thresh != null) return `임계값 ${thresh}`;
|
if (thresh != null) return `임계값 ${thresh}`;
|
||||||
return field ?? indicatorType;
|
return field ?? indicatorType;
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ import {
|
|||||||
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
|
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
|
||||||
import {
|
import {
|
||||||
activateDirectThresholdInput,
|
activateDirectThresholdInput,
|
||||||
|
buildHwpValueFieldOpt,
|
||||||
|
buildSingleIndicatorPeriodOpts,
|
||||||
ensureDirectThresholdSnapshot,
|
ensureDirectThresholdSnapshot,
|
||||||
|
VALUE_FIELD_PREFIX,
|
||||||
getCompositeFieldOpts,
|
getCompositeFieldOpts,
|
||||||
getCompositePeriodPresetOptions,
|
getCompositePeriodPresetOptions,
|
||||||
getConditionRightPeriod,
|
getConditionRightPeriod,
|
||||||
@@ -91,6 +94,9 @@ import {
|
|||||||
parseThresholdField,
|
parseThresholdField,
|
||||||
resolveFieldCustomKind,
|
resolveFieldCustomKind,
|
||||||
resolveRightFieldCustomKind,
|
resolveRightFieldCustomKind,
|
||||||
|
resolveStoredValueField,
|
||||||
|
singleIndicatorLineLabel,
|
||||||
|
singleIndicatorValuePeriodLabel,
|
||||||
getCompositeLeftCandleType,
|
getCompositeLeftCandleType,
|
||||||
getCompositeRightCandleType,
|
getCompositeRightCandleType,
|
||||||
setCompositeLeftCandleType,
|
setCompositeLeftCandleType,
|
||||||
@@ -491,37 +497,35 @@ function buildFieldOptsForSlot(
|
|||||||
switch (ind) {
|
switch (ind) {
|
||||||
case 'MACD': {
|
case 'MACD': {
|
||||||
const { mid } = th({ mid: 0 });
|
const { mid } = th({ mid: 0 });
|
||||||
|
const signalLabel = `신호선 ${DEF.macdSignal}`;
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [
|
return [
|
||||||
{ value: 'MACD_LINE', label: 'MACD 선' },
|
{ value: 'MACD_LINE', label: 'MACD선' },
|
||||||
{ value: 'SIGNAL_LINE', label: `신호선 ${DEF.macdSignal}` },
|
{ value: 'SIGNAL_LINE', label: signalLabel },
|
||||||
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{ value: 'SIGNAL_LINE', label: `신호선 ${DEF.macdSignal}` },
|
{ value: 'SIGNAL_LINE', label: signalLabel },
|
||||||
NONE,
|
NONE,
|
||||||
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
case 'RSI': {
|
case 'RSI': {
|
||||||
const rsiP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'RSI' }, DEF) : DEF.rsiPeriod;
|
const valueOpt = buildHwpValueFieldOpt('RSI', DEF, cond)!;
|
||||||
|
const signalLabel = `신호선 ${DEF.rsiSignal}`;
|
||||||
const thresholds = buildThresholdOpts(th({ over: 70, mid: 50, under: 30 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 70, mid: 50, under: 30 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [
|
return [valueOpt, { value: 'RSI_SIGNAL', label: signalLabel }];
|
||||||
{ value: 'RSI_VALUE', label: `RSI 기간 ${rsiP}` },
|
|
||||||
{ value: 'RSI_SIGNAL', label: `신호선 ${DEF.rsiSignal}` },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{ value: 'RSI_SIGNAL', label: `신호선 ${DEF.rsiSignal}` },
|
{ value: 'RSI_SIGNAL', label: signalLabel },
|
||||||
NONE,
|
NONE,
|
||||||
...thresholds,
|
...thresholds,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
case 'STOCHASTIC': {
|
case 'STOCHASTIC': {
|
||||||
const thresholds = buildThresholdOpts(th({ over: 80, mid: 50, under: 20 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 80, mid: 50, under: 20 }), overTerm, underTerm);
|
||||||
const kLabel = `Stochastic 기간${DEF.stochK}일 %K${DEF.stochSmooth}일`;
|
const kLabel = `Stochastic 기간${DEF.stochK}일 %K${DEF.stochSmooth}`;
|
||||||
const dLabel = `Stochastic %D${DEF.stochD}`;
|
const dLabel = `Stochastic %D${DEF.stochD}`;
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [
|
return [
|
||||||
@@ -537,38 +541,40 @@ function buildFieldOptsForSlot(
|
|||||||
}
|
}
|
||||||
case 'CCI': {
|
case 'CCI': {
|
||||||
const cciP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'CCI' }, DEF) : DEF.cciPeriod;
|
const cciP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'CCI' }, DEF) : DEF.cciPeriod;
|
||||||
|
const signalLabel = `신호선 ${DEF.cciSignal}`;
|
||||||
const thresholds = buildThresholdOpts(th({ over: 100, mid: 0, under: -100 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 100, mid: 0, under: -100 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [
|
return [
|
||||||
{ value: 'CCI_VALUE', label: `CCI 기간 ${cciP}` },
|
{ value: 'CCI_VALUE', label: `CCI 기간 ${cciP}` },
|
||||||
{ value: 'CCI_SIGNAL', label: `신호선 ${DEF.cciSignal}` },
|
{ value: 'CCI_SIGNAL', label: signalLabel },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{ value: 'CCI_SIGNAL', label: `신호선 ${DEF.cciSignal}` },
|
{ value: 'CCI_SIGNAL', label: signalLabel },
|
||||||
NONE,
|
NONE,
|
||||||
...thresholds,
|
...thresholds,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
case 'ADX': {
|
case 'ADX': {
|
||||||
const adxP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'ADX' }, DEF) : DEF.adxPeriod;
|
const valueOpt = buildHwpValueFieldOpt('ADX', DEF, cond)!;
|
||||||
const thresholds = buildThresholdOpts(th({ over: 40, mid: 25, under: 20 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 40, mid: 25, under: 20 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [{ value: 'ADX_VALUE', label: `ADX 기간 ${adxP}` }];
|
return [valueOpt];
|
||||||
}
|
}
|
||||||
return [NONE, ...thresholds];
|
return [NONE, ...thresholds];
|
||||||
}
|
}
|
||||||
case 'TRIX': {
|
case 'TRIX': {
|
||||||
const trixP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'TRIX' }, DEF) : DEF.trixPeriod;
|
const valueOpt = buildHwpValueFieldOpt('TRIX', DEF, cond)!;
|
||||||
|
const trmaLabel = `TRMA ${DEF.trixSignal}`;
|
||||||
const { mid } = th({ mid: 0 });
|
const { mid } = th({ mid: 0 });
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [
|
return [
|
||||||
{ value: 'TRIX_VALUE', label: `TRIX ${trixP}` },
|
valueOpt,
|
||||||
{ value: 'TRIX_SIGNAL', label: `TRMA ${DEF.trixSignal}` },
|
{ value: 'TRIX_SIGNAL', label: trmaLabel },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{ value: 'TRIX_SIGNAL', label: `TRMA ${DEF.trixSignal}` },
|
{ value: 'TRIX_SIGNAL', label: trmaLabel },
|
||||||
NONE,
|
NONE,
|
||||||
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
||||||
];
|
];
|
||||||
@@ -579,14 +585,14 @@ function buildFieldOptsForSlot(
|
|||||||
const minusLabel = `-DI 기간${DEF.dmiPeriod}일`;
|
const minusLabel = `-DI 기간${DEF.dmiPeriod}일`;
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [
|
return [
|
||||||
{ value: 'PDI', label: plusLabel },
|
|
||||||
{ value: 'MDI', label: minusLabel },
|
{ value: 'MDI', label: minusLabel },
|
||||||
|
{ value: 'PDI', label: plusLabel },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
NONE,
|
||||||
{ value: 'MDI', label: minusLabel },
|
{ value: 'MDI', label: minusLabel },
|
||||||
{ value: 'PDI', label: plusLabel },
|
{ value: 'PDI', label: plusLabel },
|
||||||
NONE,
|
|
||||||
...thresholds,
|
...thresholds,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -602,10 +608,10 @@ function buildFieldOptsForSlot(
|
|||||||
NONE,
|
NONE,
|
||||||
];
|
];
|
||||||
case 'WILLIAMS_R': {
|
case 'WILLIAMS_R': {
|
||||||
const wrP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'WILLIAMS_R' }, DEF) : DEF.williamsR;
|
const valueOpt = buildHwpValueFieldOpt('WILLIAMS_R', DEF, cond)!;
|
||||||
const thresholds = buildThresholdOpts(th({ over: -20, mid: -50, under: -80 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: -20, mid: -50, under: -80 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [{ value: 'WILLIAMS_R_VALUE', label: `williams%R 기간${wrP}` }];
|
return [valueOpt];
|
||||||
}
|
}
|
||||||
return [NONE, ...thresholds];
|
return [NONE, ...thresholds];
|
||||||
}
|
}
|
||||||
@@ -648,47 +654,42 @@ function buildFieldOptsForSlot(
|
|||||||
return slot === 'right' ? [NONE, ...lines] : lines;
|
return slot === 'right' ? [NONE, ...lines] : lines;
|
||||||
}
|
}
|
||||||
case 'PSYCHOLOGICAL': {
|
case 'PSYCHOLOGICAL': {
|
||||||
const psyP = cond
|
const periodOpts = buildSingleIndicatorPeriodOpts('PSYCHOLOGICAL', DEF, cond);
|
||||||
? getConditionValuePeriod({ ...cond, indicatorType: ind }, DEF)
|
|
||||||
: DEF.psyPeriod;
|
|
||||||
const thresholds = buildThresholdOpts(th({ over: 75, mid: 50, under: 25 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 75, mid: 50, under: 25 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [{ value: 'PSY_VALUE', label: `심리도 기간 ${psyP}` }];
|
return periodOpts;
|
||||||
}
|
}
|
||||||
return [NONE, ...thresholds];
|
return [NONE, ...thresholds];
|
||||||
}
|
}
|
||||||
case 'NEW_PSYCHOLOGICAL': {
|
case 'NEW_PSYCHOLOGICAL': {
|
||||||
const nPsyP = cond
|
const periodOpts = buildSingleIndicatorPeriodOpts('NEW_PSYCHOLOGICAL', DEF, cond);
|
||||||
? getConditionValuePeriod({ ...cond, indicatorType: ind }, DEF)
|
|
||||||
: DEF.newPsy;
|
|
||||||
const thresholds = buildThresholdOpts(th({ over: 50, mid: 0, under: -50 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 50, mid: 0, under: -50 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [{ value: 'NEW_PSY_VALUE', label: `신심리도 기간 ${nPsyP}` }];
|
return periodOpts;
|
||||||
}
|
}
|
||||||
return [NONE, ...thresholds];
|
return [NONE, ...thresholds];
|
||||||
}
|
}
|
||||||
case 'INVEST_PSYCHOLOGICAL': {
|
case 'INVEST_PSYCHOLOGICAL': {
|
||||||
const ipsyP = cond
|
const periodOpts = buildSingleIndicatorPeriodOpts('INVEST_PSYCHOLOGICAL', DEF, cond);
|
||||||
? getConditionValuePeriod({ ...cond, indicatorType: 'INVEST_PSYCHOLOGICAL' }, DEF)
|
|
||||||
: DEF.investPsy;
|
|
||||||
const thresholds = buildThresholdOpts(th({ over: 75, mid: 50, under: 25 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 75, mid: 50, under: 25 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [{ value: 'INVEST_PSY_VALUE', label: `투자심리도 기간 ${ipsyP}` }];
|
return periodOpts;
|
||||||
}
|
}
|
||||||
return [NONE, ...thresholds];
|
return [NONE, ...thresholds];
|
||||||
}
|
}
|
||||||
case 'BWI': {
|
case 'BWI': {
|
||||||
|
const periodOpts = buildSingleIndicatorPeriodOpts('BWI', DEF, cond);
|
||||||
const thresholds = buildThresholdOpts(th({ over: 80, mid: 50, under: 20 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 80, mid: 50, under: 20 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [{ value: 'BWI_VALUE', label: `BWI 기간 ${DEF.bwiPeriod}` }];
|
return periodOpts;
|
||||||
}
|
}
|
||||||
return [NONE, ...thresholds];
|
return [NONE, ...thresholds];
|
||||||
}
|
}
|
||||||
case 'VR': {
|
case 'VR': {
|
||||||
const vrP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'VR' }, DEF) : DEF.vrPeriod;
|
const periodOpts = buildSingleIndicatorPeriodOpts('VR', DEF, cond);
|
||||||
const thresholds = buildThresholdOpts(th({ over: 200, mid: 100, under: 50 }), overTerm, underTerm);
|
const thresholds = buildThresholdOpts(th({ over: 200, mid: 100, under: 50 }), overTerm, underTerm);
|
||||||
if (slot === 'left') {
|
if (slot === 'left') {
|
||||||
return [{ value: 'VR_VALUE', label: `VR 기간 ${vrP}` }];
|
return periodOpts;
|
||||||
}
|
}
|
||||||
return [NONE, ...thresholds];
|
return [NONE, ...thresholds];
|
||||||
}
|
}
|
||||||
@@ -941,13 +942,25 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
|
|||||||
return `${indName} - ${L} ${C} ${R}${tf}`;
|
return `${indName} - ${L} ${C} ${R}${tf}`;
|
||||||
}
|
}
|
||||||
const defaults = getDefaultConditionFields(c.indicatorType, 'buy', DEF);
|
const defaults = getDefaultConditionFields(c.indicatorType, 'buy', DEF);
|
||||||
const lv = resolveFieldOptionValue(c.indicatorType, c.leftField);
|
const leftStored = resolveStoredValueField(c.indicatorType, c.leftField, DEF, c);
|
||||||
|
const lv = resolveFieldOptionValue(c.indicatorType, leftStored);
|
||||||
const rvRaw = c.rightField;
|
const rvRaw = c.rightField;
|
||||||
const rv = isThresholdOverridden(c)
|
const rv = isThresholdOverridden(c)
|
||||||
? resolveFieldOptionValue(c.indicatorType, rvRaw)
|
? resolveFieldOptionValue(c.indicatorType, rvRaw)
|
||||||
: (resolveInheritedThresholdField(rvRaw, c.indicatorType, DEF.hlThresh)
|
: (resolveInheritedThresholdField(rvRaw, c.indicatorType, DEF.hlThresh)
|
||||||
?? resolveFieldOptionValue(c.indicatorType, rvRaw));
|
?? resolveFieldOptionValue(c.indicatorType, rvRaw));
|
||||||
const L = opts.find(o => o.value === lv)?.label ?? c.leftField ?? indName;
|
const L = opts.find(o => o.value === leftStored)?.label
|
||||||
|
?? opts.find(o => o.value === lv)?.label
|
||||||
|
?? (() => {
|
||||||
|
const m = leftStored?.match(/_(\d+)$/);
|
||||||
|
if (m) return singleIndicatorValuePeriodLabel(c.indicatorType, parseInt(m[1], 10));
|
||||||
|
const base = VALUE_FIELD_PREFIX[c.indicatorType];
|
||||||
|
if (base && (leftStored === base || lv === base)) {
|
||||||
|
return singleIndicatorValuePeriodLabel(c.indicatorType, getConditionValuePeriod(c, DEF));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()
|
||||||
|
?? c.leftField ?? indName;
|
||||||
const chartRef = getChartReferenceThreshold(c, 'right', defaults.r, DEF);
|
const chartRef = getChartReferenceThreshold(c, 'right', defaults.r, DEF);
|
||||||
const R = opts.find(o => o.value === rv)?.label
|
const R = opts.find(o => o.value === rv)?.label
|
||||||
?? (chartRef != null && !isThresholdOverridden(c) ? `기준(${chartRef})` : null)
|
?? (chartRef != null && !isThresholdOverridden(c) ? `기준(${chartRef})` : null)
|
||||||
@@ -1130,7 +1143,11 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
|||||||
const inheritedThresholdField = defaults.r;
|
const inheritedThresholdField = defaults.r;
|
||||||
|
|
||||||
const getLeftValue = () => {
|
const getLeftValue = () => {
|
||||||
const v = resolveFieldOptionValue(normalized.indicatorType, normalized.leftField);
|
const raw = normalized.leftField;
|
||||||
|
const resolved = resolveStoredValueField(normalized.indicatorType, raw, def, normalized);
|
||||||
|
if (resolved && leftFieldOpts.some(o => o.value === resolved)) return resolved;
|
||||||
|
if (raw && leftFieldOpts.some(o => o.value === raw)) return raw;
|
||||||
|
const v = resolveFieldOptionValue(normalized.indicatorType, raw);
|
||||||
return isValid(v) ? v! : defaults.l;
|
return isValid(v) ? v! : defaults.l;
|
||||||
};
|
};
|
||||||
const getRightValue = () => {
|
const getRightValue = () => {
|
||||||
@@ -1378,11 +1395,12 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
|||||||
<label className="sp-cond-lbl">조건대상1</label>
|
<label className="sp-cond-lbl">조건대상1</label>
|
||||||
<ComboFieldSelect
|
<ComboFieldSelect
|
||||||
options={leftFieldOpts}
|
options={leftFieldOpts}
|
||||||
fieldValue={isHoldType ? 'NONE' : (normalized.leftField ?? getLeftValue())}
|
fieldValue={isHoldType ? 'NONE' : (resolveStoredValueField(normalized.indicatorType, normalized.leftField, def, normalized) ?? getLeftValue())}
|
||||||
isPresetField={!isHoldType && (
|
isPresetField={!isHoldType && (
|
||||||
leftFieldOpts.some(o => o.value === normalized.leftField)
|
leftFieldOpts.some(o => o.value === normalized.leftField)
|
||||||
|| (leftFieldOpts.some(o => o.value === getLeftValue())
|
|| (leftFieldOpts.some(o => o.value === resolveFieldOptionValue(normalized.indicatorType, normalized.leftField))
|
||||||
&& !isValuePeriodFieldStored(normalized.indicatorType, normalized.leftField))
|
&& !isValuePeriodFieldStored(normalized.indicatorType, normalized.leftField))
|
||||||
|
|| leftFieldOpts.some(o => o.value === resolveStoredValueField(normalized.indicatorType, normalized.leftField, def, normalized))
|
||||||
)}
|
)}
|
||||||
customKind={resolveFieldCustomKind(normalized.indicatorType, normalized.leftField)}
|
customKind={resolveFieldCustomKind(normalized.indicatorType, normalized.leftField)}
|
||||||
customNumber={getConditionValuePeriod(normalized, def)}
|
customNumber={getConditionValuePeriod(normalized, def)}
|
||||||
@@ -1390,22 +1408,23 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
|||||||
min={1}
|
min={1}
|
||||||
max={500}
|
max={500}
|
||||||
disabled={isHoldType}
|
disabled={isHoldType}
|
||||||
|
customOptionLabel={singleIndicatorValuePeriodLabel(
|
||||||
|
normalized.indicatorType,
|
||||||
|
getConditionValuePeriod(normalized, def),
|
||||||
|
)}
|
||||||
onFieldChange={v => {
|
onFieldChange={v => {
|
||||||
const bases: Record<string, string> = {
|
const valuePrefix = VALUE_FIELD_PREFIX[normalized.indicatorType];
|
||||||
RSI: 'RSI_VALUE',
|
if (valuePrefix && v === valuePrefix) {
|
||||||
CCI: 'CCI_VALUE',
|
onChange(initConditionPeriodsInherit(
|
||||||
ADX: 'ADX_VALUE',
|
normalized.indicatorType,
|
||||||
WILLIAMS_R: 'WILLIAMS_R_VALUE',
|
{ ...normalized, leftField: v },
|
||||||
TRIX: 'TRIX_VALUE',
|
def,
|
||||||
VR: 'VR_VALUE',
|
));
|
||||||
PSYCHOLOGICAL: 'PSY_VALUE',
|
return;
|
||||||
NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE',
|
}
|
||||||
INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE',
|
const pMatch = v.match(/_(\d+)$/);
|
||||||
};
|
if (valuePrefix && v.startsWith(`${valuePrefix}_`) && pMatch) {
|
||||||
const base = bases[normalized.indicatorType];
|
onChange(setConditionValuePeriod(normalized, parseInt(pMatch[1], 10), def));
|
||||||
if (base && v === base) {
|
|
||||||
const p = getConditionValuePeriod(normalized, def);
|
|
||||||
onChange({ ...normalized, leftField: `${base}_${p}`, period: p, valuePeriodOverride: true });
|
|
||||||
} else {
|
} else {
|
||||||
onChange({ ...normalized, leftField: v });
|
onChange({ ...normalized, leftField: v });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user