전략편집기 시간봉 선택오류 수정
This commit is contained in:
@@ -1276,6 +1276,10 @@ html.theme-blue {
|
|||||||
background: var(--bg, #131722);
|
background: var(--bg, #131722);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
.multi-chart-grid .chart-loading--workspace,
|
||||||
|
.tv-chart-col .chart-loading--workspace {
|
||||||
|
z-index: 930;
|
||||||
|
}
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
width: 32px; height: 32px;
|
width: 32px; height: 32px;
|
||||||
border: 3px solid var(--bg4);
|
border: 3px solid var(--bg4);
|
||||||
|
|||||||
@@ -200,6 +200,9 @@ export interface ChartWorkspaceViewProps {
|
|||||||
liveQuote: ReturnType<typeof import('../utils/chartLiveQuote').resolveChartLiveQuote>;
|
liveQuote: ReturnType<typeof import('../utils/chartLiveQuote').resolveChartLiveQuote>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
chartConfigReady: boolean;
|
||||||
|
chartAreaLoading: boolean;
|
||||||
|
chartDataLoading: boolean;
|
||||||
bulkSettingsIndicators: IndicatorConfig[];
|
bulkSettingsIndicators: IndicatorConfig[];
|
||||||
chartIndicatorsForSettings: IndicatorConfig[];
|
chartIndicatorsForSettings: IndicatorConfig[];
|
||||||
getParams: ReturnType<typeof import('../hooks/useIndicatorSettings').useIndicatorSettings>['getParams'];
|
getParams: ReturnType<typeof import('../hooks/useIndicatorSettings').useIndicatorSettings>['getParams'];
|
||||||
@@ -298,6 +301,8 @@ export interface ChartWorkspaceViewProps {
|
|||||||
handleSaveCustomOverlaySelection: (slot: ChartCustomOverlaySlotId, selection: ChartCustomOverlaySelection) => void;
|
handleSaveCustomOverlaySelection: (slot: ChartCustomOverlaySlotId, selection: ChartCustomOverlaySelection) => void;
|
||||||
syncCustomOverlaysToManager: (mgr: ChartManager) => void;
|
syncCustomOverlaysToManager: (mgr: ChartManager) => void;
|
||||||
handleCandlesReady: () => void;
|
handleCandlesReady: () => void;
|
||||||
|
handleSingleChartPaintReadyChange: (ready: boolean) => void;
|
||||||
|
handleSlotPaintReadyChange: (slotIndex: number, ready: boolean) => void;
|
||||||
applyBacktestMarkersWhenReady: (payload: NonNullable<ChartWorkspaceViewProps['btMarkerPayloadRef']['current']>, attempt?: number) => void;
|
applyBacktestMarkersWhenReady: (payload: NonNullable<ChartWorkspaceViewProps['btMarkerPayloadRef']['current']>, attempt?: number) => void;
|
||||||
btRun: ReturnType<typeof import('../hooks/useBacktest').useBacktest>['run'];
|
btRun: ReturnType<typeof import('../hooks/useBacktest').useBacktest>['run'];
|
||||||
refreshPaperAccount: (forMarket?: string) => Promise<void>;
|
refreshPaperAccount: (forMarket?: string) => Promise<void>;
|
||||||
@@ -339,7 +344,7 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
|||||||
obWsStatus, spread, orderbookPrevClose, chartTicker, tradeFill,
|
obWsStatus, spread, orderbookPrevClose, chartTicker, tradeFill,
|
||||||
paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled,
|
paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled,
|
||||||
appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote,
|
appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote,
|
||||||
isLoading, error, bulkSettingsIndicators, chartIndicatorsForSettings,
|
isLoading, error, chartConfigReady, chartAreaLoading, chartDataLoading, bulkSettingsIndicators, chartIndicatorsForSettings,
|
||||||
getParams, getVisualConfig, activeSlotIndicators,
|
getParams, getVisualConfig, activeSlotIndicators,
|
||||||
setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid,
|
setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid,
|
||||||
setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible,
|
setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible,
|
||||||
@@ -368,7 +373,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
|||||||
handleTogglePriceExtremeLabels,
|
handleTogglePriceExtremeLabels,
|
||||||
handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange,
|
handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange,
|
||||||
handleSaveCustomOverlaySelection, syncCustomOverlaysToManager,
|
handleSaveCustomOverlaySelection, syncCustomOverlaysToManager,
|
||||||
handleCandlesReady, applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
|
handleCandlesReady, handleSingleChartPaintReadyChange, handleSlotPaintReadyChange,
|
||||||
|
applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
|
||||||
handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers,
|
handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers,
|
||||||
handleTimezoneChange,
|
handleTimezoneChange,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -587,8 +593,15 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
|||||||
gridTemplateColumns: layoutDef.cols,
|
gridTemplateColumns: layoutDef.cols,
|
||||||
gridTemplateRows: layoutDef.rows,
|
gridTemplateRows: layoutDef.rows,
|
||||||
gridTemplateAreas: layoutDef.areas,
|
gridTemplateAreas: layoutDef.areas,
|
||||||
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{chartAreaLoading && (
|
||||||
|
<div className="chart-loading chart-loading--chart-area chart-loading--workspace" role="status" aria-live="polite">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<span>차트 로딩중...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* 슬롯 0 = 메인 차트 영역 (a) */}
|
{/* 슬롯 0 = 메인 차트 영역 (a) */}
|
||||||
<div className="multi-slot-wrap" style={{ gridArea: 'a', minHeight: 0, minWidth: 0 }}>
|
<div className="multi-slot-wrap" style={{ gridArea: 'a', minHeight: 0, minWidth: 0 }}>
|
||||||
<ChartSlot
|
<ChartSlot
|
||||||
@@ -629,6 +642,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
|||||||
marketTickers={marketTickers}
|
marketTickers={marketTickers}
|
||||||
compactMode
|
compactMode
|
||||||
chartVisible={chartVisible}
|
chartVisible={chartVisible}
|
||||||
|
chartConfigReady={chartConfigReady}
|
||||||
|
onPaintReadyChange={handleSlotPaintReadyChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* 슬롯 1~7 */}
|
{/* 슬롯 1~7 */}
|
||||||
@@ -673,6 +688,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
|||||||
marketTickers={marketTickers}
|
marketTickers={marketTickers}
|
||||||
compactMode
|
compactMode
|
||||||
chartVisible={chartVisible}
|
chartVisible={chartVisible}
|
||||||
|
chartConfigReady={chartConfigReady}
|
||||||
|
onPaintReadyChange={handleSlotPaintReadyChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -680,7 +697,13 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── 싱글 레이아웃 (기존) ─────────────────────────────────────── */}
|
{/* ── 싱글 레이아웃 (기존) ─────────────────────────────────────── */}
|
||||||
<div className="tv-chart-col" style={{ display: layoutDef.count > 1 ? 'none' : undefined }}>
|
<div className="tv-chart-col" style={{ display: layoutDef.count > 1 ? 'none' : undefined, position: 'relative' }}>
|
||||||
|
{chartAreaLoading && (
|
||||||
|
<div className="chart-loading chart-loading--chart-area chart-loading--workspace" role="status" aria-live="polite">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<span>차트 로딩중...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ChartLegendBar
|
<ChartLegendBar
|
||||||
visibility={chartLegendOptions}
|
visibility={chartLegendOptions}
|
||||||
crosshairInfoVisible={chartCrosshairInfoVisible}
|
crosshairInfoVisible={chartCrosshairInfoVisible}
|
||||||
@@ -698,7 +721,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
|||||||
<TradingChart
|
<TradingChart
|
||||||
key={chartMountKey}
|
key={chartMountKey}
|
||||||
chartVisible={chartVisible}
|
chartVisible={chartVisible}
|
||||||
dataLoading={useUpbit && isLoading}
|
dataLoading={chartDataLoading}
|
||||||
|
onChartPaintReadyChange={handleSingleChartPaintReadyChange}
|
||||||
bars={bars} barsMarket={barsMarket} market={symbol} timeframe={timeframe} displayTimezone={displayTimezone}
|
bars={bars} barsMarket={barsMarket} market={symbol} timeframe={timeframe} displayTimezone={displayTimezone}
|
||||||
chartType={chartType} theme={theme} mode={mode}
|
chartType={chartType} theme={theme} mode={mode}
|
||||||
indicators={effectiveIndicators} drawingTool={drawingTool}
|
indicators={effectiveIndicators} drawingTool={drawingTool}
|
||||||
|
|||||||
@@ -573,6 +573,13 @@ export function useChartWorkspace({
|
|||||||
const [favoritesList, setFavoritesList] = useState<string[]>(getFavorites);
|
const [favoritesList, setFavoritesList] = useState<string[]>(getFavorites);
|
||||||
// DB 로드 완료 여부 (false → DB 로드 전, localStorage 기반 동작)
|
// DB 로드 완료 여부 (false → DB 로드 전, localStorage 기반 동작)
|
||||||
const [dbLoaded, setDbLoaded] = useState(false);
|
const [dbLoaded, setDbLoaded] = useState(false);
|
||||||
|
/** DB·앱설정·지표 기본값 반영 완료 — 이후 STOMP 초기 로딩 시작 (이중 페인트 방지) */
|
||||||
|
const [chartBootstrapComplete, setChartBootstrapComplete] = useState(false);
|
||||||
|
/** 싱글 레이아웃 차트 paint 완료 */
|
||||||
|
const [singleChartPaintReady, setSingleChartPaintReady] = useState(false);
|
||||||
|
/** 멀티 슬롯별 paint 완료 */
|
||||||
|
const slotPaintReadyRef = useRef<Record<number, boolean>>({});
|
||||||
|
const [slotPaintEpoch, setSlotPaintEpoch] = useState(0);
|
||||||
|
|
||||||
const priorityMarkets = useMemo(
|
const priorityMarkets = useMemo(
|
||||||
() => [symbol, ...watchlist],
|
() => [symbol, ...watchlist],
|
||||||
@@ -1111,6 +1118,7 @@ export function useChartWorkspace({
|
|||||||
|
|
||||||
const handleCandlesReady = useCallback(() => {
|
const handleCandlesReady = useCallback(() => {
|
||||||
chartLiveReadyRef.current = true;
|
chartLiveReadyRef.current = true;
|
||||||
|
setSingleChartPaintReady(true);
|
||||||
const pending = pendingRealtimeBarRef.current;
|
const pending = pendingRealtimeBarRef.current;
|
||||||
const mgr = managerRef.current;
|
const mgr = managerRef.current;
|
||||||
if (!pending || !mgr || barsMarketRef.current !== chartContextRef.current.symbol) return;
|
if (!pending || !mgr || barsMarketRef.current !== chartContextRef.current.symbol) return;
|
||||||
@@ -1118,16 +1126,26 @@ export function useChartWorkspace({
|
|||||||
mgr.updateBar(pending);
|
mgr.updateBar(pending);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSingleChartPaintReadyChange = useCallback((ready: boolean) => {
|
||||||
|
setSingleChartPaintReady(ready);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSlotPaintReadyChange = useCallback((slotIndex: number, ready: boolean) => {
|
||||||
|
slotPaintReadyRef.current[slotIndex] = ready;
|
||||||
|
setSlotPaintEpoch(v => v + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const chartRealtimeSource = appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP';
|
const chartRealtimeSource = appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP';
|
||||||
|
const chartConfigReady = appSettingsLoaded && dbLoaded && chartBootstrapComplete;
|
||||||
const { bars: upbitBars, barsMarket: upbitBarsMarket, latestBar, wsStatus, isLoading, error } = useChartRealtimeData(
|
const { bars: upbitBars, barsMarket: upbitBarsMarket, latestBar, wsStatus, isLoading, error } = useChartRealtimeData(
|
||||||
symbol,
|
symbol,
|
||||||
timeframe,
|
timeframe,
|
||||||
useMemo(() => ({
|
useMemo(() => ({
|
||||||
onTickUpdate: handleTickUpdate,
|
onTickUpdate: handleTickUpdate,
|
||||||
onNewCandle: handleNewCandle,
|
onNewCandle: handleNewCandle,
|
||||||
enabled: chartScreenActive && useUpbit,
|
enabled: chartScreenActive && useUpbit && chartConfigReady,
|
||||||
source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
|
source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
|
||||||
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartRealtimeSource]),
|
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartConfigReady, chartRealtimeSource]),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
|
// 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
|
||||||
@@ -1167,8 +1185,15 @@ export function useChartWorkspace({
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
chartLiveReadyRef.current = false;
|
chartLiveReadyRef.current = false;
|
||||||
pendingRealtimeBarRef.current = null;
|
pendingRealtimeBarRef.current = null;
|
||||||
|
setSingleChartPaintReady(false);
|
||||||
}, [symbol, timeframe]);
|
}, [symbol, timeframe]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
slotPaintReadyRef.current = {};
|
||||||
|
setSlotPaintEpoch(v => v + 1);
|
||||||
|
setSingleChartPaintReady(false);
|
||||||
|
}, [layoutDef.count, sessionKey]);
|
||||||
|
|
||||||
// Upbit 초기 로딩 완료 시 stats 업데이트
|
// Upbit 초기 로딩 완료 시 stats 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useUpbit && upbitBars.length > 0) {
|
if (useUpbit && upbitBars.length > 0) {
|
||||||
@@ -1379,6 +1404,10 @@ export function useChartWorkspace({
|
|||||||
});
|
});
|
||||||
}, [focusedIndicatorId, indicators, layoutDef.count]);
|
}, [focusedIndicatorId, indicators, layoutDef.count]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSingleChartPaintReady(false);
|
||||||
|
}, [effectiveIndicators]);
|
||||||
|
|
||||||
const handleExpandIndicator = useCallback((id: string) => {
|
const handleExpandIndicator = useCallback((id: string) => {
|
||||||
setFocusedIndicatorId(id);
|
setFocusedIndicatorId(id);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -1701,7 +1730,7 @@ export function useChartWorkspace({
|
|||||||
const bulkSettingsIndicators = layoutDef.count > 1 ? activeSlotIndicators : indicators;
|
const bulkSettingsIndicators = layoutDef.count > 1 ? activeSlotIndicators : indicators;
|
||||||
|
|
||||||
/** 설정 화면에서 DB 기본값 저장 후 차트 복귀 시 — 전역 기본값을 차트 인스턴스에 일괄 반영 */
|
/** 설정 화면에서 DB 기본값 저장 후 차트 복귀 시 — 전역 기본값을 차트 인스턴스에 일괄 반영 */
|
||||||
const lastSyncedSettingsRevisionRef = useRef(indicatorSettingsRevision);
|
const lastSyncedSettingsRevisionRef = useRef(-1);
|
||||||
const syncChartsFromSavedDefaults = useCallback(() => {
|
const syncChartsFromSavedDefaults = useCallback(() => {
|
||||||
const merge = (inds: IndicatorConfig[]) =>
|
const merge = (inds: IndicatorConfig[]) =>
|
||||||
mergeGlobalDefaultsOntoChartIndicators(inds, getParams, getVisualConfig);
|
mergeGlobalDefaultsOntoChartIndicators(inds, getParams, getVisualConfig);
|
||||||
@@ -1729,15 +1758,25 @@ export function useChartWorkspace({
|
|||||||
}, [layoutDef.count, activeSlot, activeSlotIndicators, getParams, getVisualConfig]);
|
}, [layoutDef.count, activeSlot, activeSlotIndicators, getParams, getVisualConfig]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!chartVisible) return;
|
if (!appSettingsLoaded || !dbLoaded) {
|
||||||
if (indicatorSettingsRevision <= lastSyncedSettingsRevisionRef.current) return;
|
setChartBootstrapComplete(false);
|
||||||
if (consumeSkipNextChartDefaultsSync()) {
|
|
||||||
lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (chartVisible && indicatorSettingsRevision > lastSyncedSettingsRevisionRef.current) {
|
||||||
|
if (consumeSkipNextChartDefaultsSync()) {
|
||||||
|
lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
|
||||||
|
} else {
|
||||||
syncChartsFromSavedDefaults();
|
syncChartsFromSavedDefaults();
|
||||||
lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
|
lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
|
||||||
}, [chartVisible, indicatorSettingsRevision, syncChartsFromSavedDefaults]);
|
}
|
||||||
|
}
|
||||||
|
setChartBootstrapComplete(true);
|
||||||
|
}, [appSettingsLoaded, dbLoaded, chartVisible, indicatorSettingsRevision, syncChartsFromSavedDefaults]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChartBootstrapComplete(false);
|
||||||
|
lastSyncedSettingsRevisionRef.current = -1;
|
||||||
|
}, [sessionKey]);
|
||||||
|
|
||||||
/** 보조지표 일괄 설정 적용 — 전체 지표 DB + 차트 인스턴스 병합 */
|
/** 보조지표 일괄 설정 적용 — 전체 지표 DB + 차트 인스턴스 병합 */
|
||||||
const handleBulkIndicatorApply = useCallback((result: {
|
const handleBulkIndicatorApply = useCallback((result: {
|
||||||
@@ -1911,6 +1950,23 @@ export function useChartWorkspace({
|
|||||||
settingsPageProps,
|
settingsPageProps,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const allSlotsPaintReady = useMemo(() => {
|
||||||
|
void slotPaintEpoch;
|
||||||
|
for (let i = 0; i < layoutDef.count; i++) {
|
||||||
|
if (!slotPaintReadyRef.current[i]) return false;
|
||||||
|
}
|
||||||
|
return layoutDef.count > 0;
|
||||||
|
}, [layoutDef.count, slotPaintEpoch]);
|
||||||
|
|
||||||
|
const chartDataFetched = !useUpbit || (!isLoading && bars.length >= 2 && barsMarket === symbol);
|
||||||
|
const chartPaintComplete = layoutDef.count > 1 ? allSlotsPaintReady : singleChartPaintReady;
|
||||||
|
const chartDataLoading = useUpbit && chartVisible && (!chartConfigReady || !chartDataFetched);
|
||||||
|
const chartAreaLoading = useUpbit && chartVisible && (
|
||||||
|
!chartConfigReady
|
||||||
|
|| !chartPaintComplete
|
||||||
|
|| (layoutDef.count === 1 && !chartDataFetched)
|
||||||
|
);
|
||||||
|
|
||||||
const viewProps: ChartWorkspaceViewProps = {
|
const viewProps: ChartWorkspaceViewProps = {
|
||||||
onOpenNotificationsPage,
|
onOpenNotificationsPage,
|
||||||
onOpenAppDownload,
|
onOpenAppDownload,
|
||||||
@@ -1946,6 +2002,7 @@ export function useChartWorkspace({
|
|||||||
paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled,
|
paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled,
|
||||||
appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote,
|
appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote,
|
||||||
isLoading, error: error ?? null, bulkSettingsIndicators, chartIndicatorsForSettings,
|
isLoading, error: error ?? null, bulkSettingsIndicators, chartIndicatorsForSettings,
|
||||||
|
chartConfigReady, chartAreaLoading, chartDataLoading,
|
||||||
getParams, getVisualConfig, activeSlotIndicators,
|
getParams, getVisualConfig, activeSlotIndicators,
|
||||||
setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid,
|
setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid,
|
||||||
setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible,
|
setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible,
|
||||||
@@ -1974,7 +2031,8 @@ export function useChartWorkspace({
|
|||||||
handleTogglePriceExtremeLabels,
|
handleTogglePriceExtremeLabels,
|
||||||
handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange,
|
handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange,
|
||||||
handleSaveCustomOverlaySelection, syncCustomOverlaysToManager,
|
handleSaveCustomOverlaySelection, syncCustomOverlaysToManager,
|
||||||
handleCandlesReady, applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
|
handleCandlesReady, handleSingleChartPaintReadyChange, handleSlotPaintReadyChange,
|
||||||
|
applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
|
||||||
handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers,
|
handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers,
|
||||||
goToMarketChart, handleTimezoneChange,
|
goToMarketChart, handleTimezoneChange,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -206,6 +206,10 @@ export interface ChartSlotProps {
|
|||||||
chartLiveReceiveHighlight?: boolean;
|
chartLiveReceiveHighlight?: boolean;
|
||||||
/** 실시간 차트 화면 표시 여부 (설정 등 다른 메뉴에서 숨김 시 false) */
|
/** 실시간 차트 화면 표시 여부 (설정 등 다른 메뉴에서 숨김 시 false) */
|
||||||
chartVisible?: boolean;
|
chartVisible?: boolean;
|
||||||
|
/** 워크스페이스·지표 설정 반영 완료 — STOMP 시작 게이트 */
|
||||||
|
chartConfigReady?: boolean;
|
||||||
|
/** paint 완료 알림 (멀티차트 전체 로딩용) */
|
||||||
|
onPaintReadyChange?: (slotIndex: number, ready: boolean) => void;
|
||||||
/** pane 구분선 */
|
/** pane 구분선 */
|
||||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||||
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 */
|
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 */
|
||||||
@@ -241,6 +245,8 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
compactMode = false,
|
compactMode = false,
|
||||||
chartLiveReceiveHighlight = true,
|
chartLiveReceiveHighlight = true,
|
||||||
chartVisible = true,
|
chartVisible = true,
|
||||||
|
chartConfigReady = true,
|
||||||
|
onPaintReadyChange,
|
||||||
chartPaneSeparator,
|
chartPaneSeparator,
|
||||||
chartCrosshairInfoVisible = true,
|
chartCrosshairInfoVisible = true,
|
||||||
chartHoverToolbarVisible = true,
|
chartHoverToolbarVisible = true,
|
||||||
@@ -508,17 +514,6 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
// ※ TradingChart를 재마운트(key 변경)하면 managerRef가 초기화되어 MarketSearchPanel 등
|
// ※ TradingChart를 재마운트(key 변경)하면 managerRef가 초기화되어 MarketSearchPanel 등
|
||||||
// 다른 상태에 영향을 줄 수 있으므로, 재마운트 대신 resetPaneHeights 재시도만 수행한다.
|
// 다른 상태에 영향을 줄 수 있으므로, 재마운트 대신 resetPaneHeights 재시도만 수행한다.
|
||||||
const slotContainerRef = useRef<HTMLDivElement | null>(null);
|
const slotContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
// 데이터 로딩 완료 후 차트가 여전히 비어 있으면 1회만 재마운트 (data + blank guard)
|
|
||||||
const [reloadTick, setReloadTick] = useState(0);
|
|
||||||
const reloadTriggeredRef = useRef(false); // 재마운트는 최대 1회
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
reloadTriggeredRef.current = false;
|
|
||||||
chartLiveReadyRef.current = false;
|
|
||||||
pendingRealtimeBarRef.current = null;
|
|
||||||
pendingSyncVisibleRef.current = null;
|
|
||||||
pendingSyncLogicalRef.current = null;
|
|
||||||
skipSyncApplyRef.current = true;
|
|
||||||
}, [symbol, timeframe]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checks = [200, 500, 1000, 2000].map(delay =>
|
const checks = [200, 500, 1000, 2000].map(delay =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -528,21 +523,13 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
const h = el.clientHeight;
|
const h = el.clientHeight;
|
||||||
if (h <= 0) return;
|
if (h <= 0) return;
|
||||||
if (mgr.hasMainSeries()) {
|
if (mgr.hasMainSeries()) {
|
||||||
// 차트 데이터 있음 → pane 높이만 재적용
|
|
||||||
mgr.resetPaneHeights(h);
|
mgr.resetPaneHeights(h);
|
||||||
} else if (!reloadTriggeredRef.current) {
|
|
||||||
// 차트 비어 있고 아직 재시도 안 했음 → 1회만 재마운트
|
|
||||||
// (bars 로딩 지연일 수 있으므로, 늦은 타이머에서만 트리거)
|
|
||||||
if (delay >= 1000) {
|
|
||||||
reloadTriggeredRef.current = true;
|
|
||||||
setReloadTick(t => t + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, delay)
|
}, delay)
|
||||||
);
|
);
|
||||||
return () => checks.forEach(clearTimeout);
|
return () => checks.forEach(clearTimeout);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [symbol, timeframe]);
|
||||||
|
|
||||||
// ── 데이터 ────────────────────────────────────────────────────────────
|
// ── 데이터 ────────────────────────────────────────────────────────────
|
||||||
const useUpbit = isUpbitMarket(symbol);
|
const useUpbit = isUpbitMarket(symbol);
|
||||||
@@ -567,12 +554,26 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
|
|
||||||
const handleCandlesReady = useCallback(() => {
|
const handleCandlesReady = useCallback(() => {
|
||||||
chartLiveReadyRef.current = true;
|
chartLiveReadyRef.current = true;
|
||||||
|
onPaintReadyChange?.(slotIndex, true);
|
||||||
const pending = pendingRealtimeBarRef.current;
|
const pending = pendingRealtimeBarRef.current;
|
||||||
const mgr = managerRef.current;
|
const mgr = managerRef.current;
|
||||||
if (!pending || !mgr || barsMarketRef.current !== symbolRef.current) return;
|
if (!pending || !mgr || barsMarketRef.current !== symbolRef.current) return;
|
||||||
pendingRealtimeBarRef.current = null;
|
pendingRealtimeBarRef.current = null;
|
||||||
mgr.updateBar(pending);
|
mgr.updateBar(pending);
|
||||||
}, []);
|
}, [slotIndex, onPaintReadyChange]);
|
||||||
|
|
||||||
|
const handleSlotPaintReadyChange = useCallback((ready: boolean) => {
|
||||||
|
onPaintReadyChange?.(slotIndex, ready);
|
||||||
|
}, [slotIndex, onPaintReadyChange]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
chartLiveReadyRef.current = false;
|
||||||
|
pendingRealtimeBarRef.current = null;
|
||||||
|
pendingSyncVisibleRef.current = null;
|
||||||
|
pendingSyncLogicalRef.current = null;
|
||||||
|
skipSyncApplyRef.current = true;
|
||||||
|
onPaintReadyChange?.(slotIndex, false);
|
||||||
|
}, [symbol, timeframe, slotIndex, onPaintReadyChange]);
|
||||||
|
|
||||||
const handleTickUpdate = useCallback((bar: OHLCVBar) => {
|
const handleTickUpdate = useCallback((bar: OHLCVBar) => {
|
||||||
if (barsMarketRef.current !== symbolRef.current) return;
|
if (barsMarketRef.current !== symbolRef.current) return;
|
||||||
@@ -604,15 +605,18 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
useMemo(() => ({
|
useMemo(() => ({
|
||||||
onTickUpdate: handleTickUpdate,
|
onTickUpdate: handleTickUpdate,
|
||||||
onNewCandle: handleNewCandle,
|
onNewCandle: handleNewCandle,
|
||||||
enabled: useUpbit && chartVisible,
|
enabled: useUpbit && chartVisible && chartConfigReady,
|
||||||
source: chartRealtimeSource,
|
source: chartRealtimeSource,
|
||||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartVisible, chartRealtimeSource]),
|
}), [handleTickUpdate, handleNewCandle, useUpbit, chartVisible, chartConfigReady, chartRealtimeSource]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bars = useUpbit ? upbitBars : simBars;
|
const bars = useUpbit ? upbitBars : simBars;
|
||||||
const barsMarket = useUpbit ? upbitBarsMarket : symbol;
|
const barsMarket = useUpbit ? upbitBarsMarket : symbol;
|
||||||
barsMarketRef.current = barsMarket;
|
barsMarketRef.current = barsMarket;
|
||||||
|
|
||||||
|
const slotDataFetched = !useUpbit || (!isLoading && bars.length >= 2 && barsMarket === symbol);
|
||||||
|
const slotChartLoading = useUpbit && chartVisible && (!chartConfigReady || !slotDataFetched);
|
||||||
|
|
||||||
const lastKnownBar = useUpbit
|
const lastKnownBar = useUpbit
|
||||||
? (latestBar ?? (bars.length > 0 ? bars[bars.length - 1] : null))
|
? (latestBar ?? (bars.length > 0 ? bars[bars.length - 1] : null))
|
||||||
: (bars.length > 0 ? bars[bars.length - 1] : null);
|
: (bars.length > 0 ? bars[bars.length - 1] : null);
|
||||||
@@ -966,15 +970,6 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
className={compactMode ? 'vtd-card-chart-canvas slot-chart-wrap slot-chart-wrap--compact' : 'slot-chart-wrap'}
|
className={compactMode ? 'vtd-card-chart-canvas slot-chart-wrap slot-chart-wrap--compact' : 'slot-chart-wrap'}
|
||||||
style={{ flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
|
style={{ flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
|
||||||
>
|
>
|
||||||
{useUpbit && isLoading && (
|
|
||||||
compactMode ? (
|
|
||||||
<div className="vtd-card-chart-loading">차트 로딩…</div>
|
|
||||||
) : (
|
|
||||||
<div className="chart-loading" style={{ position: 'absolute', zIndex: 10 }}>
|
|
||||||
<div className="loading-spinner" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
{isLoadingMore && (
|
{isLoadingMore && (
|
||||||
<div className="chart-history-loading">
|
<div className="chart-history-loading">
|
||||||
<div className="loading-spinner" style={{ width: 14, height: 14 }} />
|
<div className="loading-spinner" style={{ width: 14, height: 14 }} />
|
||||||
@@ -983,8 +978,9 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<TradingChart
|
<TradingChart
|
||||||
key={reloadTick}
|
|
||||||
chartVisible={chartVisible}
|
chartVisible={chartVisible}
|
||||||
|
dataLoading={slotChartLoading}
|
||||||
|
onChartPaintReadyChange={handleSlotPaintReadyChange}
|
||||||
bars={bars}
|
bars={bars}
|
||||||
barsMarket={barsMarket}
|
barsMarket={barsMarket}
|
||||||
market={symbol}
|
market={symbol}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
decodeConditionForEditor,
|
decodeConditionForEditor,
|
||||||
alignBuySellStartCandleTypesForSave,
|
alignBuySellStartCandleTypesForSave,
|
||||||
encodeConditionForSave,
|
encodeConditionForSave,
|
||||||
syncBuySellPrimaryStartCandleTypes,
|
|
||||||
mergeStartMetaForLoad,
|
mergeStartMetaForLoad,
|
||||||
addExtraStartSection,
|
addExtraStartSection,
|
||||||
hasMultipleStartSections,
|
hasMultipleStartSections,
|
||||||
@@ -726,36 +725,8 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
}, [setCurrentRoot, setCurrentLayout, scheduleStrategyPersist]);
|
}, [setCurrentRoot, setCurrentLayout, scheduleStrategyPersist]);
|
||||||
|
|
||||||
const handleStartCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
|
const handleStartCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
|
||||||
if (startId === START_NODE_ID) {
|
handleEditorStateChange(updateStartCandleTypes(currentEditorState, startId, candleTypes));
|
||||||
const synced = syncBuySellPrimaryStartCandleTypes(buyEditorState, sellEditorState, candleTypes);
|
}, [handleEditorStateChange, currentEditorState]);
|
||||||
setBuyCondition(synced.buy.root);
|
|
||||||
setBuyLayout(prev => ({
|
|
||||||
...prev,
|
|
||||||
startMeta: synced.buy.startMeta,
|
|
||||||
extraStartIds: synced.buy.extraStartIds,
|
|
||||||
extraRoots: synced.buy.extraRoots,
|
|
||||||
startCombineOp: normalizeStartCombineOp(synced.buy.startCombineOp),
|
|
||||||
}));
|
|
||||||
setSellCondition(synced.sell.root);
|
|
||||||
setSellLayout(prev => ({
|
|
||||||
...prev,
|
|
||||||
startMeta: synced.sell.startMeta,
|
|
||||||
extraStartIds: synced.sell.extraStartIds,
|
|
||||||
extraRoots: synced.sell.extraRoots,
|
|
||||||
startCombineOp: normalizeStartCombineOp(synced.sell.startCombineOp),
|
|
||||||
}));
|
|
||||||
scheduleStrategyPersist();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const state = signalTab === 'buy' ? buyEditorState : sellEditorState;
|
|
||||||
handleEditorStateChange(updateStartCandleTypes(state, startId, candleTypes));
|
|
||||||
}, [
|
|
||||||
buyEditorState,
|
|
||||||
sellEditorState,
|
|
||||||
handleEditorStateChange,
|
|
||||||
scheduleStrategyPersist,
|
|
||||||
signalTab,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleStartCombineOpChange = useCallback((op: StartCombineOp) => {
|
const handleStartCombineOpChange = useCallback((op: StartCombineOp) => {
|
||||||
handleEditorStateChange(updateStartCombineOp(currentEditorState, op));
|
handleEditorStateChange(updateStartCombineOp(currentEditorState, op));
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ interface TradingChartProps {
|
|||||||
crosshairInfoVisible?: boolean;
|
crosshairInfoVisible?: boolean;
|
||||||
/** 외부 캔들 데이터 fetch 중 (실시간 차트 초기 로딩) */
|
/** 외부 캔들 데이터 fetch 중 (실시간 차트 초기 로딩) */
|
||||||
dataLoading?: boolean;
|
dataLoading?: boolean;
|
||||||
|
/** chartPaintReady 변경 알림 (워크스페이스 전체 로딩 오버레이용) */
|
||||||
|
onChartPaintReadyChange?: (ready: boolean) => void;
|
||||||
/** 마운트 시 캔들 pane 전체보기(하단 보조지표 pane 숨김) — 가상매매 카드 등 */
|
/** 마운트 시 캔들 pane 전체보기(하단 보조지표 pane 숨김) — 가상매매 카드 등 */
|
||||||
defaultCandleOnly?: boolean;
|
defaultCandleOnly?: boolean;
|
||||||
/** 캔들 pane 좌하단 전체보기/복원 버튼 (기본 true) */
|
/** 캔들 pane 좌하단 전체보기/복원 버튼 (기본 true) */
|
||||||
@@ -236,6 +238,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
showCandleOverlayControls = false,
|
showCandleOverlayControls = false,
|
||||||
crosshairInfoVisible = true,
|
crosshairInfoVisible = true,
|
||||||
dataLoading = false,
|
dataLoading = false,
|
||||||
|
onChartPaintReadyChange,
|
||||||
defaultCandleOnly = false,
|
defaultCandleOnly = false,
|
||||||
showCandlePaneControls = true,
|
showCandlePaneControls = true,
|
||||||
candleOverlayToggles,
|
candleOverlayToggles,
|
||||||
@@ -399,6 +402,14 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
chartVisibleRef.current = chartVisible;
|
chartVisibleRef.current = chartVisible;
|
||||||
}, [chartVisible]);
|
}, [chartVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dataLoading) setChartPaintReady(false);
|
||||||
|
}, [dataLoading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChartPaintReadyChange?.(chartPaintReady && !dataLoading);
|
||||||
|
}, [chartPaintReady, dataLoading, onChartPaintReadyChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -976,12 +987,18 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
} while (reloadCoalesceRef.current);
|
} while (reloadCoalesceRef.current);
|
||||||
} finally {
|
} finally {
|
||||||
reloadInFlightRef.current = false;
|
reloadInFlightRef.current = false;
|
||||||
if (managerRef.current?.hasMainSeries()) {
|
|
||||||
setChartPaintReady(true);
|
|
||||||
}
|
|
||||||
if (reloadCoalesceRef.current) {
|
if (reloadCoalesceRef.current) {
|
||||||
reloadCoalesceRef.current = false;
|
reloadCoalesceRef.current = false;
|
||||||
queueFullReload();
|
queueFullReload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const m = managerRef.current;
|
||||||
|
if (m?.hasMainSeries()) {
|
||||||
|
const expected = countExpectedVisibleIndicators(indicatorsRef.current);
|
||||||
|
const loaded = m.getLoadedIndicatorCount();
|
||||||
|
if (expected === 0 || loaded >= expected) {
|
||||||
|
setChartPaintReady(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
}, [normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
||||||
|
|||||||
@@ -204,11 +204,6 @@ function normalizeCandleTypesList(types: string[]): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (out.length === 0) return [DEFAULT_START_CANDLE];
|
if (out.length === 0) return [DEFAULT_START_CANDLE];
|
||||||
// 레거시/불일치로 multi 분봉에 stale 1m 이 섞인 경우가 있어,
|
|
||||||
// multi 인데 non-1m 이 있으면 1m 을 기본적으로 제외한다.
|
|
||||||
if (out.length > 1 && out.includes('1m') && out.some(ct => ct !== '1m')) {
|
|
||||||
return out.filter(ct => ct !== '1m');
|
|
||||||
}
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +409,7 @@ export function collectTimeframesFromEditorState(state: EditorConditionState): s
|
|||||||
return [...set];
|
return [...set];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 매수·매도 START(기본) 평가 분봉을 동일하게 맞춤 — 한쪽만 3m·5m 체크 시 다른 쪽 1m 잔존 방지 */
|
/** @deprecated 매수·매도 시간봉은 각각 독립 설정 — updateStartCandleTypes 사용 */
|
||||||
export function syncBuySellPrimaryStartCandleTypes(
|
export function syncBuySellPrimaryStartCandleTypes(
|
||||||
buy: EditorConditionState,
|
buy: EditorConditionState,
|
||||||
sell: EditorConditionState,
|
sell: EditorConditionState,
|
||||||
@@ -436,19 +431,14 @@ export function syncEditorStateCandleTypesForSave(state: EditorConditionState):
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 저장 직전 — 양쪽 START 분봉 합집합을 각 탭에 반영 (매수만 설정한 경우 매도 DSL도 동기화) */
|
/** 저장 직전 — 각 탭 START 분봉을 해당 조건 트리 left/rightCandleType 에 반영 (매수·매도 독립) */
|
||||||
export function alignBuySellStartCandleTypesForSave(
|
export function alignBuySellStartCandleTypesForSave(
|
||||||
buy: EditorConditionState,
|
buy: EditorConditionState,
|
||||||
sell: EditorConditionState,
|
sell: EditorConditionState,
|
||||||
): { buy: EditorConditionState; sell: EditorConditionState } {
|
): { buy: EditorConditionState; sell: EditorConditionState } {
|
||||||
const merged = normalizeCandleTypesList([
|
|
||||||
...getStartCandleTypes(buy.startMeta[START_NODE_ID]),
|
|
||||||
...getStartCandleTypes(sell.startMeta[START_NODE_ID]),
|
|
||||||
]);
|
|
||||||
const synced = syncBuySellPrimaryStartCandleTypes(buy, sell, merged);
|
|
||||||
return {
|
return {
|
||||||
buy: syncEditorStateCandleTypesForSave(synced.buy),
|
buy: syncEditorStateCandleTypesForSave(buy),
|
||||||
sell: syncEditorStateCandleTypesForSave(synced.sell),
|
sell: syncEditorStateCandleTypesForSave(sell),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { generateNodeId } from './strategyTypes';
|
|||||||
|
|
||||||
export const START_NODE_ID = '__strategy_start__';
|
export const START_NODE_ID = '__strategy_start__';
|
||||||
export const DEFAULT_START_CANDLE = '1m';
|
export const DEFAULT_START_CANDLE = '1m';
|
||||||
|
/** 새 전략 START 노드 기본 평가 분봉 */
|
||||||
|
export const DEFAULT_NEW_STRATEGY_START_CANDLE = '3m';
|
||||||
|
|
||||||
export const STRATEGY_CANDLE_TYPES = [
|
export const STRATEGY_CANDLE_TYPES = [
|
||||||
'1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1w', '1d',
|
'1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1w', '1d',
|
||||||
@@ -36,7 +38,12 @@ export function createStartNodeId(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function defaultStartMeta(): Record<string, StartNodeMeta> {
|
export function defaultStartMeta(): Record<string, StartNodeMeta> {
|
||||||
return { [START_NODE_ID]: { candleTypes: [DEFAULT_START_CANDLE], candleType: DEFAULT_START_CANDLE } };
|
return {
|
||||||
|
[START_NODE_ID]: {
|
||||||
|
candleTypes: [DEFAULT_NEW_STRATEGY_START_CANDLE],
|
||||||
|
candleType: DEFAULT_NEW_STRATEGY_START_CANDLE,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** START 메타에서 정렬된 평가 분봉 목록 (최소 1개) */
|
/** START 메타에서 정렬된 평가 분봉 목록 (최소 1개) */
|
||||||
|
|||||||
Reference in New Issue
Block a user