전략편집기 복합지표 기능 반영

This commit is contained in:
Macbook
2026-05-25 01:34:52 +09:00
parent ca249ad318
commit 20dd257c29
21 changed files with 1781 additions and 183 deletions
+108 -24
View File
@@ -154,6 +154,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
const prevChartType = useRef<ChartType>(chartType);
const prevTheme = useRef<Theme>(theme);
const prevLogScale = useRef<boolean>(logScale);
const indicatorsRef = useRef(indicators);
const recoveryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [chartMgr, setChartMgr] = useState<ChartManager | null>(null);
/** 캔들 pane 전체보기 (오버레이 지표 유지, 하단 보조지표 pane 숨김) */
@@ -163,6 +165,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
barsRef.current = bars;
}, [bars]);
useEffect(() => {
indicatorsRef.current = indicators;
}, [indicators]);
const toggleCandleOnly = useCallback(() => {
setCandleOnlyMode(v => !v);
}, []);
@@ -259,6 +265,49 @@ const TradingChart: React.FC<TradingChartProps> = ({
const reloadSafetyTimers = useRef<ReturnType<typeof setTimeout>[]>([]);
const reloadGenRef = useRef(0);
const reloadRetryRef = useRef(0);
const reloadAllRef = useRef<(
mgr: ChartManager,
newBars: OHLCVBar[],
ct: ChartType,
th: Theme,
ls: boolean,
inds: IndicatorConfig[],
) => Promise<void>>(async () => {});
const queueIndicatorRecovery = useCallback(() => {
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
recoveryTimerRef.current = window.setTimeout(() => {
recoveryTimerRef.current = null;
const m = managerRef.current;
const barData = barsRef.current;
const inds = indicatorsRef.current;
if (!m || barData.length === 0) return;
if (barsMarket !== undefined && barsMarket !== market) return;
const expected = countExpectedVisibleIndicators(inds);
if (expected === 0) return;
if (m.getLoadedIndicatorCount() >= expected) return;
prevBarsKey.current = '';
const ct = prevChartType.current;
const th = prevTheme.current;
const ls = prevLogScale.current;
if (m.hasMainSeries()) {
void m.reloadIndicatorsOnly(inds).then(() => {
if (m.getLoadedIndicatorCount() >= expected) {
prevBarsKey.current = barsKey(barData, market);
prevIndKey.current = indKey(inds);
applyPaneLayout(m);
return;
}
reloadAllRef.current?.(m, barData, ct, th, ls, inds);
});
return;
}
reloadAllRef.current?.(m, barData, ct, th, ls, inds);
}, 120);
}, [market, barsMarket, applyPaneLayout]);
const reloadAll = useCallback(async (
mgr: ChartManager,
@@ -280,6 +329,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
mgr.setData(newBars, ct, th);
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
queueIndicatorRecovery();
return;
}
mgr.setTheme(th);
@@ -289,6 +339,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
for (const ind of inds) {
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
queueIndicatorRecovery();
return;
}
await mgr.addIndicator(ind);
@@ -296,21 +347,30 @@ const TradingChart: React.FC<TradingChartProps> = ({
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
queueIndicatorRecovery();
return;
}
const expectIndicators = inds.some(i => i.hidden !== true);
if (expectIndicators && mgr.getIndicatorCount() === 0) {
const expected = countExpectedVisibleIndicators(inds);
const loaded = mgr.getLoadedIndicatorCount();
if (expected > 0 && loaded < expected) {
prevBarsKey.current = '';
if (reloadRetryRef.current < 3) {
if (reloadRetryRef.current < 4 && mgr.hasMainSeries()) {
reloadRetryRef.current += 1;
requestAnimationFrame(() => {
if (managerRef.current) {
void reloadAll(managerRef.current, newBars, ct, th, ls, inds);
}
});
await mgr.reloadIndicatorsOnly(inds);
if (gen !== reloadGenRef.current) {
queueIndicatorRecovery();
return;
}
}
if (mgr.getLoadedIndicatorCount() < expected) {
if (reloadRetryRef.current < 6) {
reloadRetryRef.current += 1;
queueIndicatorRecovery();
}
return;
}
return;
}
reloadRetryRef.current = 0;
@@ -332,6 +392,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
queueIndicatorRecovery();
return;
}
@@ -341,6 +402,12 @@ const TradingChart: React.FC<TradingChartProps> = ({
setTimeout(() => {
const m = managerRef.current;
if (!m || !m.hasMainSeries()) return;
const expectedLater = countExpectedVisibleIndicators(indicatorsRef.current);
if (expectedLater > 0 && m.getLoadedIndicatorCount() < expectedLater) {
prevBarsKey.current = '';
queueIndicatorRecovery();
return;
}
const w = wrapperRef.current;
if (!w || w.clientHeight <= 0) return;
m.resetPaneHeights(w.clientHeight);
@@ -349,7 +416,11 @@ const TradingChart: React.FC<TradingChartProps> = ({
if (delay === 1400) onDataLoaded?.();
}, delay)
);
}, [applyPaneLayout, onDataLoaded, displayTimezone, timeframe, market]);
}, [applyPaneLayout, onDataLoaded, displayTimezone, timeframe, market, queueIndicatorRecovery]);
useEffect(() => {
reloadAllRef.current = reloadAll;
}, [reloadAll]);
// ── 차트 초기화 (마운트 시 1회) ──────────────────────────────────────────
useEffect(() => {
@@ -633,6 +704,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
}
reloadSafetyTimers.current.forEach(clearTimeout);
reloadSafetyTimers.current = [];
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
ro.disconnect();
managerRef.current?.destroy();
managerRef.current = null;
@@ -682,10 +754,11 @@ const TradingChart: React.FC<TradingChartProps> = ({
const ctChanged = chartType !== prevChartType.current;
const thChanged = theme !== prevTheme.current;
const lsChanged = logScale !== prevLogScale.current;
const expectIndicators = indicators.some(i => i.hidden !== true);
const indicatorsIncomplete = expectIndicators
const expectIndicators = countExpectedVisibleIndicators(indicators);
const loadedIndicators = mgr.getLoadedIndicatorCount();
const indicatorsIncomplete = expectIndicators > 0
&& mgr.hasMainSeries()
&& mgr.getIndicatorCount() === 0;
&& loadedIndicators < expectIndicators;
if (!barsChanged && !indChanged && !ctChanged && !thChanged && !lsChanged && !indicatorsIncomplete) return;
@@ -778,18 +851,24 @@ const TradingChart: React.FC<TradingChartProps> = ({
if (!mgr || !chartMgr || bars.length === 0) return;
if (barsMarket !== undefined && barsMarket !== market) return;
const expectIndicators = indicators.some(i => i.hidden !== true);
if (!expectIndicators) return;
const expected = countExpectedVisibleIndicators(indicators);
if (expected === 0) return;
if (mgr.getLoadedIndicatorCount() >= expected) return;
const t = window.setTimeout(() => {
const m = managerRef.current;
if (!m || !m.hasMainSeries() || m.getIndicatorCount() > 0) return;
prevBarsKey.current = '';
void reloadAll(m, bars, chartType, theme, logScale, indicators);
}, 120);
const timers = [120, 400, 900].map(delay =>
window.setTimeout(() => {
const m = managerRef.current;
if (!m || !m.hasMainSeries()) return;
if (barsMarket !== undefined && barsMarket !== market) return;
const exp = countExpectedVisibleIndicators(indicatorsRef.current);
if (exp === 0 || m.getLoadedIndicatorCount() >= exp) return;
prevBarsKey.current = '';
queueIndicatorRecovery();
}, delay),
);
return () => window.clearTimeout(t);
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, reloadAll]);
return () => { timers.forEach(clearTimeout); };
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, queueIndicatorRecovery]);
// cursor 모드에서 드로잉 위에 있으면 pointer 커서로 변경
const handleWrapperMouseMove = useCallback((e: React.PointerEvent) => {
@@ -988,10 +1067,15 @@ const PaneLegendPortal: React.FC<
};
// ── 변경 감지용 키 생성 헬퍼 ────────────────────────────────────────────────
function countExpectedVisibleIndicators(inds: IndicatorConfig[]): number {
return inds.filter(i => i.hidden !== true).length;
}
function barsKey(bars: OHLCVBar[], market = ''): string {
if (bars.length === 0) return '';
const last = bars[bars.length - 1];
return `${market}:${bars.length}:${bars[0].time}:${last.time}:${last.close}`;
// last.close 제외 — 틱마다 full reload 방지 (실시간은 updateBar/appendBar 경로)
return `${market}:${bars.length}:${bars[0].time}:${last.time}`;
}
/**