Загрузка данных


diff --git a/CHANGELOG.md b/CHANGELOG.md
index 350244472a449dcbda4a0f23aab70e8262db69eb..164add7ec5722b354f9c9f0148b2a74519e9dfd8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # latest
 
+- Добавлен empty state для индикаторов при отсутствии основного инструмента
+
+# 0.1.26
+
 - Исправлены расчёты и начальный размер короткой и длинной позиций
 
 # 0.1.25
diff --git a/jest.config.ts b/jest.config.ts
index a679886f216e6a9dd480a8040300cfde2b33628c..6098bca898cb6c4907c9779ebb3ec934a8989196 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -36,7 +36,7 @@ const config: Config.InitialOptions = {
     '^lightweight-charts$': '<rootDir>/node_modules/lightweight-charts/dist/lightweight-charts.development.mjs',
     '^exchange-elements/v2$': '<rootDir>/node_modules/exchange-elements/dist/v2.cjs.js',
     '\\.module\\.(?:css|scss|sass)$': 'identity-obj-proxy',
-    '\\.(?:css|scss|sass)$': '<rootDir>/__mocks__/styleMock.cjs',
+    '\\.(?:css|scss|sass)$': '<rootDir>/__mocks__/styleMock.js',
   },
   coverageDirectory: '<rootDir>/coverage',
   coverageReporters: ['text', 'lcov'],
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
index 1fb40c1338213bc812320ddb4c0df0478e0fd04c..47e0a3b6b9fe9d2c39d358f82b126d8662592026 100644
--- a/src/components/Footer/index.tsx
+++ b/src/components/Footer/index.tsx
@@ -5,14 +5,13 @@ import { Button, Tooltip } from 'exchange-elements/v2';
 import { useEffect, useState } from 'react';
 import { Observable } from 'rxjs';
 
+import { CHART_SCROLLABLE_CLASSNAME } from '@src/constants';
 import { t } from '@src/translations';
 
 import { Intervals, IntervalsToTimeframe, Timeframes } from '@src/types';
 
 import { formatUtcOffset, useObservable } from '@src/utils';
 
-import coreStyles from '../../core/styles.module.scss';
-
 import styles from './index.module.scss';
 
 interface FooterProps {
@@ -35,7 +34,7 @@ export function Footer({ setInterval: setIntervalValue, intervalObs, supportedTi
 
   return (
     <footer className={styles.footer}>
-      <div className={classNames(styles.intervals, coreStyles.scrollableBox)}>
+      <div className={classNames(styles.intervals, CHART_SCROLLABLE_CLASSNAME)}>
         {(Object.keys(Intervals) as Intervals[])
           .filter((interval: Intervals) => {
             if (!IntervalsToTimeframe[interval]) {
diff --git a/src/constants/dom.ts b/src/constants/dom.ts
index 4faddbff9f9c6ec04272dca67f5c0650631b7799..440bce1a8e6732f81dffc1a2171ed1459cac21b0 100644
--- a/src/constants/dom.ts
+++ b/src/constants/dom.ts
@@ -4,6 +4,7 @@ export const CHART_PORTAL_HOST_CLASSNAME = 'moex-chart-portal-host';
 export const CHART_MODAL_CONTAINER_CLASSNAME = 'moex-chart-modal-container';
 
 export const CHART_PANE_OVERLAY_CONTAINER = 'moex-chart-pane-overlay-container';
+export const CHART_PANE_EMPTY_STATE_CONTAINER = 'moex-chart-pane-empty-state-container';
 
 export const CHART_DRAWING_TOOLBAR_CONTAINER = 'moex-chart-drawing-toolbar-container';
 export const CHART_DRAWING_TOOLTIP = 'moex-chart-drawing-tooltip';
@@ -11,3 +12,6 @@ export const CHART_DRAWING_TOOLTIP = 'moex-chart-drawing-tooltip';
 export const CHART_PRICE_SCALE_CONTROLS = 'moex-price-scale-controls';
 export const CHART_PRICE_SCALE_CONTROLS_CONTENT = `${CHART_PRICE_SCALE_CONTROLS}-content`;
 export const CHART_PRICE_SCALE_CONTROLS_VISIBLE = `${CHART_PRICE_SCALE_CONTROLS}_visible`;
+
+export const CHART_FULLSCREEN_SAFARI_CLASSNAME = 'moex-chart-fullscreen-safari';
+export const CHART_SCROLLABLE_CLASSNAME = 'moex-chart-scrollable';
diff --git a/src/core/Chart.ts b/src/core/Chart.ts
index 9f9fab81bdd77a6ca66af5497f0170c5e42b723d..c614d810d1569e3d7b7ebb5db924a7bdd5aa0723 100644
--- a/src/core/Chart.ts
+++ b/src/core/Chart.ts
@@ -382,7 +382,9 @@ export class Chart implements ISerializable<ChartSnapshot> {
   }
 
   private scheduleHistoryBatch = () => {
-    if (this.historyBatchRunning) return;
+    if (this.historyBatchRunning || this.activeSymbolIds.length === 0) {
+      return;
+    }
 
     this.historyBatchRunning = true;
 
@@ -427,7 +429,11 @@ export class Chart implements ISerializable<ChartSnapshot> {
     };
 
     const symbolIds$ = combineLatest([this.eventManager.symbolId(), this.compareManager.itemsObs()]).pipe(
-      map(([mainSymbolId, items]) => Array.from(new Set([mainSymbolId, ...items.map(({ symbolId }) => symbolId)]))),
+      map(([mainSymbolId, items]) =>
+        Array.from(
+          new Set([mainSymbolId, ...items.map(({ symbolId }) => symbolId)].filter((symbolId) => symbolId !== '')),
+        ),
+      ),
     );
 
     this.subscriptions.add(
@@ -437,7 +443,9 @@ export class Chart implements ISerializable<ChartSnapshot> {
         .subscribe(([interval, symbolIds]) => {
           this.currentInterval = interval;
 
-          if (!interval) return;
+          if (!interval || symbolIds.length === 0) {
+            return;
+          }
 
           if (interval === Intervals.All) {
             Promise.all(symbolIds.map((symbolId) => this.dataSource.loadAllHistory(symbolId)))
diff --git a/src/core/CompareManager.ts b/src/core/CompareManager.ts
index e06d6226cc5654ba89b6fb81ddde3333e551942b..761eb13dd7e09a3c832defb93b13e80c60047d42 100644
--- a/src/core/CompareManager.ts
+++ b/src/core/CompareManager.ts
@@ -1,6 +1,4 @@
 import { IChartApi, PriceScaleMode, SeriesType } from 'lightweight-charts';
-
-import { flatten } from 'lodash-es';
 import { BehaviorSubject, distinctUntilChanged, map, Observable, of, Subscription } from 'rxjs';
 
 import { DataSource } from '@core/DataSource';
@@ -12,7 +10,13 @@ import { PriceScale } from '@core/PriceScale';
 import { COMPARE_COLOR_PALETTE } from '@src/theme';
 import { CompareItem, CompareMode, Direction, IndicatorConfig, SymbolInfo, SymbolInfoInput } from '@src/types';
 import { CompareSnapshot } from '@src/types/snapshot';
-import { createFallbackColor, normalizeColor, normalizeSymbol, normalizeSymbolInfo } from '@src/utils';
+import {
+  createFallbackColor,
+  getIndicatorColors,
+  normalizeColor,
+  normalizeSymbol,
+  normalizeSymbolInfo,
+} from '@src/utils';
 
 type MainScaleCompareMode = CompareMode.Absolute | CompareMode.Percentage;
 
@@ -40,7 +44,6 @@ export class CompareManager {
   private readonly itemsSubject = new BehaviorSubject<CompareItem[]>([]);
   private readonly entitiesSubject = new BehaviorSubject<Indicator[]>([]);
   private readonly subscriptions = new Subscription();
-
   private percentageComparisonActive = false;
   private restoringInitialIndicators = false;
 
@@ -122,23 +125,12 @@ export class CompareManager {
     }
 
     const entity = this.indicatorManager.addEntity<Indicator>((zIndex, moveUp, moveDown) => {
-      const usedColorsByCompare = this.entitiesSubject.value.map(
-        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-        // @ts-ignore
-        (indicator) => indicator.getConfig().series?.[0]?.seriesOptions?.color,
-      );
-
       const existingIndicators = Array.from(this.indicatorManager.getIndicators().value.values());
 
-      const usedColorsByIndicatorsRaw = existingIndicators.map((indicator) =>
-        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-        // @ts-ignore
-        indicator.config?.series?.map((series) => series.seriesOptions?.color),
+      const usedColors = [...this.entitiesSubject.value, ...existingIndicators].flatMap((indicator) =>
+        getIndicatorColors(indicator.getConfig()),
       );
 
-      const usedColorsByIndicators = flatten(usedColorsByIndicatorsRaw).filter((color) => color !== undefined);
-      const usedColors = usedColorsByCompare.concat(usedColorsByIndicators);
-
       const config = getDefaultCompareIndicatorConfig(symbolInfo, usedColors);
 
       const associatedPane =
@@ -270,7 +262,6 @@ export class CompareManager {
     try {
       for (const compareIndicator of compareIndicators) {
         const { scale, symbolInfo, seriesName, paneId } = compareIndicator;
-
         const symbolInfoNormalized = normalizeSymbolInfo(symbolInfo);
 
         if (!symbolInfoNormalized) {
@@ -285,7 +276,7 @@ export class CompareManager {
           scale === Direction.Left ? CompareMode.NewScale : mainPaneId === paneId ? mainScaleMode : CompareMode.NewPane;
 
         // eslint-disable-next-line no-await-in-loop
-        await this.setSymbolMode(seriesName, symbolInfoNormalized, compareMode, paneId!);
+        await this.setSymbolMode(seriesName, symbolInfoNormalized, compareMode, paneId);
       }
     } finally {
       this.restoringInitialIndicators = false;
@@ -368,7 +359,6 @@ export class CompareManager {
     const mainRightPriceScale = this.paneManager.getMainPane().getPriceScale(Direction.Right);
 
     this.syncPercentageMode(mainRightPriceScale, percentageComparisonActive);
-
     this.paneManager.invalidate();
   }
 }
diff --git a/src/core/ContainerManager.ts b/src/core/ContainerManager.ts
index 79caa2e4193f11383dd338070449a0e2602da452..4b42ac0ef55869ed9c67435b69abc4b56030694c 100644
--- a/src/core/ContainerManager.ts
+++ b/src/core/ContainerManager.ts
@@ -1,12 +1,12 @@
 import {
   CHART_DRAWING_TOOLBAR_CONTAINER,
   CHART_MODAL_CONTAINER_CLASSNAME,
+  CHART_PANE_EMPTY_STATE_CONTAINER,
   CHART_PANE_OVERLAY_CONTAINER,
   CHART_ROOT_CLASSNAME,
+  CHART_SCROLLABLE_CLASSNAME,
 } from '@src/constants';
 
-import styles from './styles.module.scss';
-
 interface CreateContainersOptions {
   parentContainer: HTMLElement;
   showBottomPanel?: boolean;
@@ -67,10 +67,10 @@ export class ContainerManager {
       : `${headerHeight}px minmax(0, 1fr)`;
 
     const headerContainer = document.createElement('div');
+    headerContainer.classList.add(CHART_SCROLLABLE_CLASSNAME);
     headerContainer.style.width = '100%';
     headerContainer.style.height = `${headerHeight}px`;
     headerContainer.style.overflow = 'auto hidden';
-    headerContainer.className = styles.scrollableBox;
 
     const footerContainer = document.createElement('div');
     footerContainer.style.width = '100%';
@@ -152,11 +152,9 @@ export class ContainerManager {
     return {
       headerContainer,
       footerContainer,
-
       chartContainer,
       chartAreaContainer,
       toolBarContainer,
-
       modalContainer,
       controlBarContainer,
       drawingToolbarContainer,
@@ -187,9 +185,14 @@ export class ContainerManager {
     paneOverlayContainer.style.zIndex = ZIndex.Base;
     paneOverlayContainer.style.pointerEvents = 'none';
 
+    const paneEmptyStateContainer = document.createElement('div');
+    paneEmptyStateContainer.classList.add(CHART_PANE_EMPTY_STATE_CONTAINER);
+    paneEmptyStateContainer.hidden = true;
+
     return {
       legendContainer,
       paneOverlayContainer,
+      paneEmptyStateContainer,
     };
   }
 }
diff --git a/src/core/EventManager.ts b/src/core/EventManager.ts
index 53a9f14bc001b82d515c8edce38be84b42d32e40..0271d545b73e9282d5a075162dafa04528929905 100644
--- a/src/core/EventManager.ts
+++ b/src/core/EventManager.ts
@@ -1,7 +1,6 @@
 import { BehaviorSubject, combineLatest, distinctUntilChanged, map, Observable, Subscription } from 'rxjs';
 
 import { ChartOptionsModel, ChartSeriesType, Intervals, SymbolInfo, SymbolInfoInput, TimeFormat } from '@src/types';
-
 import { Defaults } from '@src/types/defaults';
 import { Timeframes } from '@src/types/timeframes';
 import { DateFormat, getTimeframeByInterval, normalizeSymbolInfo, shouldShowTime } from '@src/utils';
@@ -22,10 +21,15 @@ interface SetWithHistoryOptions {
   history?: boolean;
 }
 
+const EMPTY_SYMBOL_INFO: SymbolInfo = {
+  symbolId: '',
+  symbol: '',
+  symbolName: '',
+};
+
 /**
- * Менеджер (настроек)*, которые меняются во время использование
- * Отвечает за централизованное управление (настроек)
- * * имеются в виду настройки, которые пользователь применяет к графику
+ * Менеджер настроек, которые меняются во время использования.
+ * Отвечает за централизованное управление настройками графика.
  */
 export class EventManager {
   private timeframe$: BehaviorSubject<Timeframes>;
@@ -34,9 +38,7 @@ export class EventManager {
   private timeFormat$: BehaviorSubject<TimeFormat>;
   private dateFormat$: BehaviorSubject<DateFormat>;
   private interval$: BehaviorSubject<Intervals | null>;
-
   private controlBarVisible$ = new BehaviorSubject<boolean>(false); // todo: move to render
-
   private undoRedo: UndoRedo;
 
   constructor({
@@ -49,13 +51,9 @@ export class EventManager {
   }: EventManagerParams) {
     const normalizedSymbolInfo = normalizeSymbolInfo(initialSymbolInfo);
 
-    if (!normalizedSymbolInfo) {
-      throw new Error('[EventManager] symbolId is required');
-    }
-
     this.timeframe$ = new BehaviorSubject<Timeframes>(initialTimeframe);
     this.seriesSelected$ = new BehaviorSubject<ChartSeriesType>(initialSeries);
-    this.symbolInfo$ = new BehaviorSubject<SymbolInfo>(normalizedSymbolInfo);
+    this.symbolInfo$ = new BehaviorSubject<SymbolInfo>(normalizedSymbolInfo ?? EMPTY_SYMBOL_INFO);
     this.timeFormat$ = new BehaviorSubject<TimeFormat>(initialTimeFormat ?? Defaults.timeFormat);
     this.dateFormat$ = new BehaviorSubject<DateFormat>(initialDateFormat ?? Defaults.dateFormat);
     this.interval$ = new BehaviorSubject<Intervals | null>(initialInterval);
@@ -135,6 +133,21 @@ export class EventManager {
     this.setWithHistory('symbolInfo', this.symbolInfo$, nextSymbolInfo, options);
   };
 
+  public clearSymbol = (options?: SetWithHistoryOptions): void => {
+    if (!this.symbolInfo$.value.symbolId) {
+      return;
+    }
+
+    this.setWithHistory('symbolInfo', this.symbolInfo$, EMPTY_SYMBOL_INFO, options);
+  };
+
+  public hasSymbol(): Observable<boolean> {
+    return this.symbolInfo$.pipe(
+      map(({ symbolId }) => Boolean(symbolId)),
+      distinctUntilChanged(),
+    );
+  }
+
   public symbolId(): Observable<string> {
     return this.symbolInfo$.pipe(
       map(({ symbolId }) => symbolId),
@@ -223,29 +236,38 @@ export class EventManager {
 
   public importChartSettings(settings: ChartSettingsSource): void {
     const { symbolInfo, seriesSelected, timeframe, timeFormat, dateFormat, interval } = parseChartSettings(settings);
-
     const setOptions = { history: false };
 
     if (symbolInfo) {
-      this.setSymbol(symbolInfo, setOptions);
+      if (symbolInfo.symbolId) {
+        this.setSymbol(symbolInfo, setOptions);
+      } else {
+        this.clearSymbol(setOptions);
+      }
     }
+
     if (seriesSelected) {
       this.setSeriesSelected(seriesSelected, setOptions);
     }
+
     if (timeFormat) {
       this.setTimeFormat(timeFormat, setOptions);
     }
+
     if (dateFormat) {
       this.setDateFormat(dateFormat, setOptions);
     }
+
     if (interval != null) {
       this.setInterval(interval, setOptions);
       return;
     }
+
     if (timeframe) {
       this.setTimeframe(timeframe, setOptions);
       return;
     }
+
     if (interval === null) {
       this.resetInterval(setOptions);
     }
diff --git a/src/core/Fullscreen.ts b/src/core/Fullscreen.ts
index 354a4f954475a05abdf8551de00fa9825a77c703..67dc8adca1c9ac6477d3a43ec3df45dc102dbbd7 100644
--- a/src/core/Fullscreen.ts
+++ b/src/core/Fullscreen.ts
@@ -1,4 +1,4 @@
-import styles from './styles.module.scss';
+import { CHART_FULLSCREEN_SAFARI_CLASSNAME } from '@src/constants';
 
 export class FullscreenController {
   private isFull = false;
@@ -33,7 +33,7 @@ export class FullscreenController {
     if (this.isFullscreen) return;
 
     if (this.isSafari()) {
-      this.element.classList.add(styles.safariFullscreen);
+      this.element.classList.add(CHART_FULLSCREEN_SAFARI_CLASSNAME);
     } else {
       await this.element.requestFullscreen();
     }
@@ -45,7 +45,7 @@ export class FullscreenController {
     if (!this.isFullscreen) return;
 
     if (this.isSafari()) {
-      this.element.classList.remove(styles.safariFullscreen);
+      this.element.classList.remove(CHART_FULLSCREEN_SAFARI_CLASSNAME);
     } else {
       await document.exitFullscreen();
     }
diff --git a/src/core/IndicatorManager.ts b/src/core/IndicatorManager.ts
index ee4b55953d180a47d20a31188cc5bf91aa742d98..2b44eabafcd0a1846631a45fbff9b46f2c22df72 100644
--- a/src/core/IndicatorManager.ts
+++ b/src/core/IndicatorManager.ts
@@ -1,5 +1,5 @@
 import { IChartApi } from 'lightweight-charts';
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, map, Observable } from 'rxjs';
 
 import { DataSource } from '@core/DataSource';
 import { DOMModel } from '@core/DOMModel';
@@ -13,7 +13,7 @@ import { ChartTypeOptions, IndicatorConfig } from '@src/types';
 import { IndicatorSnapshot } from '@src/types/snapshot';
 import { applyNextIndicatorColors, getIndicatorColors } from '@src/utils';
 
-interface SeriesParams {
+interface IndicatorManagerParams {
   eventManager: EventManager;
   dataSource: DataSource;
   lwcChart: IChartApi;
@@ -24,17 +24,23 @@ interface SeriesParams {
 }
 
 export class IndicatorManager {
-  private eventManager: EventManager;
-  private lwcChart: IChartApi;
-  private chartOptions?: ChartTypeOptions;
-
-  private entities$: BehaviorSubject<Indicator[]> = new BehaviorSubject<Indicator[]>([]);
-  private indicatorsMap$: BehaviorSubject<Map<string, Indicator>> = new BehaviorSubject(new Map()); // todo: заменить IndicatorsIds ключ на уникальный id индикатора
-  private DOM: DOMModel;
-  private dataSource: DataSource;
-  private paneManager: PaneManager;
-
-  constructor({ eventManager, dataSource, lwcChart, DOM, chartOptions, initialIndicators, paneManager }: SeriesParams) {
+  private readonly eventManager: EventManager;
+  private readonly lwcChart: IChartApi;
+  private readonly chartOptions?: ChartTypeOptions;
+  private readonly indicatorsMap$ = new BehaviorSubject<Map<string, Indicator>>(new Map());
+  private readonly DOM: DOMModel;
+  private readonly dataSource: DataSource;
+  private readonly paneManager: PaneManager;
+
+  constructor({
+    eventManager,
+    dataSource,
+    lwcChart,
+    DOM,
+    chartOptions,
+    initialIndicators,
+    paneManager,
+  }: IndicatorManagerParams) {
     this.eventManager = eventManager;
     this.lwcChart = lwcChart;
     this.chartOptions = chartOptions;
@@ -42,10 +48,8 @@ export class IndicatorManager {
     this.dataSource = dataSource;
     this.paneManager = paneManager;
 
-    this.indicatorsMap$ = new BehaviorSubject<Map<string, Indicator>>(new Map());
-
-    initialIndicators?.forEach((ind) => {
-      this.addIndicator(ind);
+    initialIndicators?.forEach((indicator) => {
+      this.addIndicator(indicator);
     });
   }
 
@@ -60,10 +64,9 @@ export class IndicatorManager {
       console.error('[IndicatorManager] Не был получен тип индиктора');
       return;
     }
-    const indicatorsMap = new Map(this.indicatorsMap$.value);
 
+    const indicatorsMap = new Map(this.indicatorsMap$.value);
     const id = snap.id ?? `${snap.indicatorType}-${crypto.randomUUID()}`;
-
     const config = getConfigByIndicatorType({
       indicatorType: snap.indicatorType,
       existedIndicators: this.indicatorsMap$.value,
@@ -72,39 +75,35 @@ export class IndicatorManager {
     const associatedPane =
       snap.paneId !== undefined
         ? (this.paneManager.getPaneById(snap.paneId) ?? this.paneManager.addPane())
-        : config?.newPane
+        : config.newPane
           ? this.paneManager.addPane()
           : this.paneManager.getMainPane();
 
-    const indicatorToSet = this.addEntity<Indicator>(
-      (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) => {
-        return new Indicator({
-          id,
-          paneId: associatedPane.getId(),
-          zIndex,
-          onDelete: this.deleteIndicator,
-          moveUp,
-          moveDown,
-          mainSymbolId$: this.eventManager.symbolId(),
-          mainSymbol$: this.eventManager.symbol(),
-          lwcChart: this.lwcChart,
-          dataSource: this.dataSource,
-          associatedPane,
-          config,
-          settings: snap.settings,
-          type: snap.indicatorType,
-          chartOptions: this.chartOptions,
-        });
-      },
-    );
+    const indicatorToSet = this.addEntity<Indicator>((zIndex, moveUp, moveDown) => {
+      return new Indicator({
+        id,
+        paneId: associatedPane.getId(),
+        zIndex,
+        onDelete: this.deleteIndicator,
+        moveUp,
+        moveDown,
+        mainSymbolId$: this.eventManager.symbolId(),
+        mainSymbol$: this.eventManager.symbol(),
+        lwcChart: this.lwcChart,
+        dataSource: this.dataSource,
+        associatedPane,
+        config,
+        settings: snap.settings,
+        type: snap.indicatorType,
+        chartOptions: this.chartOptions,
+      });
+    });
 
     indicatorsMap.set(id, indicatorToSet);
-
     this.indicatorsMap$.next(indicatorsMap);
-    this.entities$.next(Array.from(indicatorsMap.values()));
   }
 
-  public getIndicators() {
+  public getIndicators(): BehaviorSubject<Map<string, Indicator>> {
     return this.indicatorsMap$;
   }
 
@@ -112,24 +111,24 @@ export class IndicatorManager {
     this.DOM.removeEntity(entity);
   }
 
-  public deleteIndicator = (id: string) => {
-    const indicatorsMap = new Map(this.indicatorsMap$.value);
-    const entity = indicatorsMap.get(id);
+  public deleteIndicator = (id: string): void => {
+    const indicator = this.indicatorsMap$.value.get(id);
 
-    if (!entity) {
+    if (!indicator) {
       return;
     }
 
-    this.removeEntity(entity);
-    entity.destroy();
-    indicatorsMap.delete(id);
+    const indicators = new Map(this.indicatorsMap$.value);
+    indicators.delete(id);
 
-    this.indicatorsMap$.next(indicatorsMap);
-    this.entities$.next(Array.from(indicatorsMap.values()));
+    this.indicatorsMap$.next(indicators);
+
+    this.removeEntity(indicator);
+    indicator.destroy();
   };
 
   public entities(): Observable<Indicator[]> {
-    return this.entities$.asObservable();
+    return this.indicatorsMap$.pipe(map((indicators) => Array.from(indicators.values())));
   }
 }
 
@@ -138,12 +137,15 @@ function getConfigByIndicatorType({
   existedIndicators,
 }: {
   indicatorType: IndicatorsIds;
-  existedIndicators: Map<string, Indicator>;
+  existedIndicators: ReadonlyMap<string, Indicator>;
 }): IndicatorConfig {
-  const configWithAppliesSettings = indicatorsConfigMap()[indicatorType] as IndicatorConfig;
-  const usedColors = getUsedIndicatorColorsByType({ indicatorType, existedIndicators });
+  const configWithAppliedSettings = indicatorsConfigMap()[indicatorType] as IndicatorConfig;
+  const usedColors = getUsedIndicatorColorsByType({
+    indicatorType,
+    existedIndicators,
+  });
 
-  return applyNextIndicatorColors(configWithAppliesSettings, usedColors);
+  return applyNextIndicatorColors(configWithAppliedSettings, usedColors);
 }
 
 function getUsedIndicatorColorsByType({
@@ -151,7 +153,7 @@ function getUsedIndicatorColorsByType({
   existedIndicators,
 }: {
   indicatorType: IndicatorSnapshot['indicatorType'];
-  existedIndicators: Map<string, Indicator>;
+  existedIndicators: ReadonlyMap<string, Indicator>;
 }): string[] {
   const colors: string[] = [];
 
diff --git a/src/core/MoexChart.tsx b/src/core/MoexChart.tsx
index 030740b1a3d036fb0cce6aa272cbeefd4b99af61..42eabbd1c8a5d1512b4d6ddb9ce9df491e2fab57 100644
--- a/src/core/MoexChart.tsx
+++ b/src/core/MoexChart.tsx
@@ -139,7 +139,7 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
       initialTimeframe: timeframe,
       initialSeries: chartSeriesType,
       initialSymbolInfo: {
-        symbolId,
+        symbolId: symbolId ?? '',
         symbol,
         symbolName,
       },
@@ -304,6 +304,10 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
     this.eventManager.setSymbol(symbolInfo);
   }
 
+  public clearSymbol(): void {
+    this.eventManager.clearSymbol();
+  }
+
   private renderAttachments(config: IMoexChart, toggleToolbar: () => boolean) {
     const drawingsCollectionManager = this.chart.getDrawingsCollectionManager();
 
diff --git a/src/core/Pane.tsx b/src/core/Pane.tsx
index a41bb3b0323ad8aaf45fc6bc057ad19e43612fb7..0484870b14f536ffdf4d930fc32eccb47c1a5545 100644
--- a/src/core/Pane.tsx
+++ b/src/core/Pane.tsx
@@ -1,6 +1,5 @@
 import { IChartApi, IPaneApi, PriceScaleMode, Time } from 'lightweight-charts';
-
-import { BehaviorSubject, Subscription } from 'rxjs';
+import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
 
 import { ChartTooltip } from '@components/ChartTooltip';
 import { LegendComponent } from '@components/Legend';
@@ -70,7 +69,6 @@ export class Pane implements ISerializable<PaneSnapshot> {
   private readonly id: number;
   private readonly isMain: boolean;
   private mainSeries = new BehaviorSubject<SeriesStrategies | null>(null); // Main Series. Exists in a single copy
-
   // todo: Отвратительный нейминг. Нужно оставить главной только эту серию и переименовать её localMainSeries => mainSerie
   // Если вдруг понадобится главная серия главного пейна, то брать из КоллекцииПейнов(-сейчас PaneManager)
   // serie to attach drawings
@@ -84,13 +82,13 @@ export class Pane implements ISerializable<PaneSnapshot> {
   private readonly drawingsManager: DrawingsManager;
   private legendContainer!: HTMLElement;
   private paneOverlayContainer!: HTMLElement;
+  private paneEmptyStateContainer!: HTMLElement;
   private legendRenderer!: UIRenderer;
   private tooltipRenderer: UIRenderer | undefined;
   private readonly modalRenderer: ModalRenderer;
   private readonly leftPriceScale: PriceScale;
   private readonly rightPriceScale: PriceScale;
   private readonly priceScaleControls: PriceScaleControls;
-  private mainSerieSub?: Subscription;
   private readonly subscribeChartEvent: ChartMouseEvents['subscribe'];
   private readonly onDelete: () => void;
   private readonly onPriceScaleStateChange: () => void;
@@ -157,7 +155,6 @@ export class Pane implements ISerializable<PaneSnapshot> {
     });
 
     this.tooltipRenderer = new ReactRenderer(this.paneOverlayContainer);
-
     this.tooltipRenderer.renderComponent(
       <ChartTooltip
         formatObs={this.eventManager.getChartOptionsModel()}
@@ -171,13 +168,26 @@ export class Pane implements ISerializable<PaneSnapshot> {
       this.initializeMainSerie({ lwcChart, dataSource });
     } else if (basedOn) {
       this.mainSeries = basedOn.getMainSerie();
-      this.mainSeries.subscribe(() => {
-        this.rebindIndicators();
-      });
     } else {
       console.error('[Pane]: There is no any mainSerie for new pane');
     }
 
+    this.subscriptions.add(
+      this.mainSeries.subscribe(() => {
+        this.rebindIndicators();
+      }),
+    );
+
+    this.subscriptions.add(
+      combineLatest([this.eventManager.hasSymbol(), this.indicatorsMap]).subscribe(([hasMainSymbol, indicators]) => {
+        const hasIndicator = Array.from(indicators.values()).some(
+          (indicator) => indicator.getIndicatorType() !== undefined,
+        );
+
+        this.paneEmptyStateContainer.hidden = hasMainSymbol || !hasIndicator;
+      }),
+    );
+
     this.drawingsManager = new DrawingsManager({
       // todo: менеджер дровингов должен быть один на чарт, не на пейн
       pane: this,
@@ -198,6 +208,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
     addDrawingManager(this.drawingsManager, this.id);
 
     this.subscriptions.add(() => removeDrawingManager(this.id));
+
     this.subscriptions.add(
       this.drawingsManager.entities().subscribe((drawings) => {
         const hasRuler = drawings.some((drawing) => drawing.getDrawingName() === DrawingsNames.ruler);
@@ -264,21 +275,27 @@ export class Pane implements ISerializable<PaneSnapshot> {
   }
 
   public setIndicator(indicatorId: string, indicator: Indicator): void {
-    const map = this.indicatorsMap.value;
+    const indicators = new Map(this.indicatorsMap.value);
 
-    map.set(indicatorId, indicator);
-    this.indicatorsMap.next(map);
+    indicators.set(indicatorId, indicator);
+
+    this.indicatorsMap.next(indicators);
     this.priceScaleControls.refresh();
   }
 
   public removeIndicator(indicatorId: string): void {
-    const map = this.indicatorsMap.value;
+    if (!this.indicatorsMap.value.has(indicatorId)) {
+      return;
+    }
+
+    const indicators = new Map(this.indicatorsMap.value);
 
-    map.delete(indicatorId);
-    this.indicatorsMap.next(map);
+    indicators.delete(indicatorId);
+
+    this.indicatorsMap.next(indicators);
     this.priceScaleControls.refresh();
 
-    if (map.size === 0 && !this.isMain) {
+    if (indicators.size === 0 && !this.isMain) {
       this.onDelete();
     }
   }
@@ -331,17 +348,16 @@ export class Pane implements ISerializable<PaneSnapshot> {
     }
 
     this.drawingsManager.destroy();
-
     this.subscriptions.unsubscribe();
     this.tooltip?.destroy();
     this.legend?.destroy();
     this.legendRenderer.destroy();
     this.tooltipRenderer?.destroy();
     this.priceScaleControls.destroy();
+    this.paneEmptyStateContainer.remove();
     this.legendContainer.remove();
     this.paneOverlayContainer.remove();
     this.indicatorsMap.complete();
-    this.mainSerieSub?.unsubscribe();
     this.localMainSeries.complete();
 
     if (this.isMain) {
@@ -400,10 +416,12 @@ export class Pane implements ISerializable<PaneSnapshot> {
   };
 
   private initializeLegend({ ohlcConfig }: { ohlcConfig: OHLCConfig }): void {
-    const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
+    const { legendContainer, paneOverlayContainer, paneEmptyStateContainer } = ContainerManager.createPaneContainers();
 
     this.legendContainer = legendContainer;
     this.paneOverlayContainer = paneOverlayContainer;
+    this.paneEmptyStateContainer = paneEmptyStateContainer;
+    this.paneEmptyStateContainer.textContent = t('Add a main instrument to display indicators');
     this.legendRenderer = new ReactRenderer(legendContainer);
 
     this.schedulePaneContainerSync();
@@ -460,22 +478,23 @@ export class Pane implements ISerializable<PaneSnapshot> {
 
   private initializeMainSerie({ lwcChart, dataSource }: { lwcChart: IChartApi; dataSource: DataSource }): void {
     this.localMainSeries = this.mainSeries;
-    this.mainSerieSub = this.eventManager.subscribeSeriesSelected((nextSeries) => {
-      this.mainSeries.value?.destroy();
-
-      const next = ensureDefined(SeriesFactory.create(nextSeries))({
-        lwcChart,
-        dataSource,
-        mainSymbolId$: this.eventManager.symbolId(),
-        mainSymbol$: this.eventManager.symbol(),
-        mainSerie$: this.mainSeries,
-      });
 
-      this.mainSeries.next(next);
-      this.rebindIndicators();
-
-      this.priceScaleControls.refresh();
-    });
+    this.subscriptions.add(
+      this.eventManager.subscribeSeriesSelected((nextSeries) => {
+        this.mainSeries.value?.destroy();
+
+        const next = ensureDefined(SeriesFactory.create(nextSeries))({
+          lwcChart,
+          dataSource,
+          mainSymbolId$: this.eventManager.symbolId(),
+          mainSymbol$: this.eventManager.symbol(),
+          mainSerie$: this.mainSeries,
+        });
+
+        this.mainSeries.next(next);
+        this.priceScaleControls.refresh();
+      }),
+    );
   }
 
   private syncPaneContainers(): void {
@@ -483,7 +502,6 @@ export class Pane implements ISerializable<PaneSnapshot> {
 
     if (!lwcPaneElement) {
       this.schedulePaneContainerSync();
-
       return;
     }
 
@@ -502,6 +520,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
 
     chartCell.style.position = 'relative';
     chartCell.appendChild(this.legendContainer);
+    chartCell.appendChild(this.paneEmptyStateContainer);
     chartCell.appendChild(this.paneOverlayContainer);
 
     this.priceScaleControls.mount(lwcPaneElement);
diff --git a/src/core/PaneManager.ts b/src/core/PaneManager.ts
index 8c8609f094b542b0aa8bd071b522e485515942a8..a2e59cbc7ffd88a421c57287c24a23be18694fa3 100644
--- a/src/core/PaneManager.ts
+++ b/src/core/PaneManager.ts
@@ -96,6 +96,10 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
         return;
       }
 
+      if (paneSnapshot.indicators.length === 0 && paneSnapshot.drawings.length === 0) {
+        return;
+      }
+
       const pane = this.addPane(undefined, paneSnapshot.id, paneSnapshot.priceScales);
 
       pane.setDrawingsSnapshot(paneSnapshot.drawings);
diff --git a/src/core/__tests__/Chart.test.ts b/src/core/__tests__/Chart.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d79c2f42d2604b7c7ce088e6db85ee4d7113c76c
--- /dev/null
+++ b/src/core/__tests__/Chart.test.ts
@@ -0,0 +1,70 @@
+import { BehaviorSubject, Subscription } from 'rxjs';
+
+import { Chart } from '@core/Chart';
+
+interface ChartInternals {
+  eventManager: {
+    symbolId: () => BehaviorSubject<string>;
+    getInterval: () => BehaviorSubject<null>;
+  };
+  compareManager: {
+    itemsObs: () => BehaviorSubject<{ symbolId: string }[]>;
+  };
+  dataSource: {
+    setSymbols: jest.Mock;
+    loadTill: jest.Mock;
+  };
+  lwcChart: {
+    timeScale: () => {
+      getVisibleRange: () => null;
+    };
+  };
+  subscriptions: Subscription;
+  activeSymbolIds: string[];
+  currentInterval: null;
+  setupDataSourceSubs: () => void;
+}
+
+describe('Chart', () => {
+  function createChartInternals(): ChartInternals {
+    return Object.assign(Object.create(Chart.prototype), {
+      eventManager: {
+        symbolId: () => new BehaviorSubject(''),
+        getInterval: () => new BehaviorSubject(null),
+      },
+      compareManager: {
+        itemsObs: () =>
+          new BehaviorSubject([
+            {
+              symbolId: 'SBER',
+            },
+            {
+              symbolId: 'SBER',
+            },
+          ]),
+      },
+      dataSource: {
+        setSymbols: jest.fn(),
+        loadTill: jest.fn(() => Promise.resolve()),
+      },
+      lwcChart: {
+        timeScale: () => ({
+          getVisibleRange: () => null,
+        }),
+      },
+      subscriptions: new Subscription(),
+      activeSymbolIds: [],
+      currentInterval: null,
+    }) as ChartInternals;
+  }
+
+  it('должен исключать пустой основной symbolId и удалять дубликаты', () => {
+    const chart = createChartInternals();
+
+    chart.setupDataSourceSubs();
+
+    expect(chart.dataSource.setSymbols).toHaveBeenLastCalledWith(['SBER']);
+
+    chart.subscriptions.unsubscribe();
+  });
+});
diff --git a/src/core/__tests__/CompareManager.test.ts b/src/core/__tests__/CompareManager.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d74b26ad73169714e107275177954abdf069c77
--- /dev/null
+++ b/src/core/__tests__/CompareManager.test.ts
@@ -0,0 +1,284 @@
+import { IChartApi, PriceScaleMode, SeriesType } from 'lightweight-charts';
+import { BehaviorSubject } from 'rxjs';
+
+import { DataSource } from '@core/DataSource';
+import { EventManager } from '@core/EventManager';
+import { Indicator } from '@core/Indicator';
+import { IndicatorManager } from '@core/IndicatorManager';
+import { Pane } from '@core/Pane';
+import { PaneManager } from '@core/PaneManager';
+import { PriceScale } from '@core/PriceScale';
+
+import { CompareManager } from '@src/core/CompareManager';
+import { CompareMode, Direction } from '@src/types';
+import { PriceScaleSide } from '@src/types/snapshot';
+import { Timeframes } from '@src/types/timeframes';
+
+jest.mock('@core/Indicator');
+
+type IndicatorFactory = (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) => Indicator;
+
+describe('CompareManager', () => {
+  const IndicatorMock = Indicator as jest.MockedClass<typeof Indicator>;
+
+  let manager: CompareManager;
+  let mainPane: Pane;
+  let secondaryPane: Pane;
+  let rightPriceScale: PriceScale;
+
+  let addEntity: jest.Mock;
+  let getIndicators: jest.Mock;
+  let removeEntity: jest.Mock;
+
+  let getMainPane: jest.Mock;
+  let getPaneById: jest.Mock;
+  let addPane: jest.Mock;
+  let setPriceScaleSideVisible: jest.Mock;
+  let invalidate: jest.Mock;
+
+  let waitUntilReady: jest.Mock;
+
+  beforeAll(() => {
+    Object.defineProperty(globalThis, 'crypto', {
+      configurable: true,
+      value: {
+        randomUUID: () => 'test-uuid',
+      },
+    });
+  });
+
+  beforeEach(() => {
+    IndicatorMock.mockImplementation(
+      (params) =>
+        ({
+          id: params.id,
+          destroy: jest.fn(),
+          getConfig: jest.fn(() => params.config),
+          getIndicatorType: jest.fn(() => params.type),
+        }) as unknown as Indicator,
+    );
+
+    rightPriceScale = {
+      getMode: jest.fn(() => PriceScaleMode.Normal),
+      setMode: jest.fn(),
+    } as unknown as PriceScale;
+
+    mainPane = {
+      getId: jest.fn(() => 0),
+      getPriceScale: jest.fn((_side: PriceScaleSide) => rightPriceScale),
+    } as unknown as Pane;
+
+    secondaryPane = {
+      getId: jest.fn(() => 1),
+    } as unknown as Pane;
+
+    addEntity = jest.fn((factory: IndicatorFactory) => factory(1, jest.fn(), jest.fn()));
+    getIndicators = jest.fn(() => new BehaviorSubject<Map<string, Indicator>>(new Map()));
+    removeEntity = jest.fn();
+
+    getMainPane = jest.fn(() => mainPane);
+    getPaneById = jest.fn();
+    addPane = jest.fn(() => secondaryPane);
+    setPriceScaleSideVisible = jest.fn();
+    invalidate = jest.fn();
+
+    waitUntilReady = jest.fn((_symbolRaw: string) => Promise.resolve());
+
+    const indicatorManager = {
+      addEntity,
+      getIndicators,
+      removeEntity,
+    } as unknown as IndicatorManager;
+
+    const paneManager = {
+      getMainPane,
+      getPaneById,
+      addPane,
+      setPriceScaleSideVisible,
+      invalidate,
+    } as unknown as PaneManager;
+
+    const dataSource = {
+      waitUntilReady,
+    } as unknown as DataSource;
+
+    const eventManager = {
+      timeframe: jest.fn(() => new BehaviorSubject(Timeframes['10s'])),
+    } as unknown as EventManager;
+
+    manager = new CompareManager({
+      chart: {} as IChartApi,
+      eventManager,
+      dataSource,
+      indicatorManager,
+      paneManager,
+    });
+  });
+
+  afterEach(() => {
+    manager.destroy();
+  });
+
+  it('должен добавлять compare-серию типа Line', async () => {
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+        symbol: 'SBER',
+        symbolName: 'Sberbank',
+      },
+      CompareMode.Absolute,
+    );
+
+    expect(IndicatorMock).toHaveBeenCalledTimes(1);
+
+    const params = IndicatorMock.mock.calls[0][0];
+
+    expect(params.config.series[0]?.name).toBe('Line');
+    expect(params.config.series[0]?.id).toBe('compare-test-uuid');
+    expect(params.associatedPane).toBe(mainPane);
+    expect(waitUntilReady).toHaveBeenCalledWith('SBER');
+    expect(manager.getAllEntities()).toHaveLength(1);
+  });
+
+  it('не должен добавлять невалидный инструмент', async () => {
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: '',
+      },
+      CompareMode.Absolute,
+    );
+
+    expect(addEntity).not.toHaveBeenCalled();
+    expect(manager.getAllEntities()).toHaveLength(0);
+  });
+
+  it('должен создавать отдельный pane для режима NewPane', async () => {
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+      },
+      CompareMode.NewPane,
+    );
+
+    expect(addPane).toHaveBeenCalledTimes(1);
+    expect(IndicatorMock.mock.calls[0][0].associatedPane).toBe(secondaryPane);
+  });
+
+  it('должен использовать существующий pane при восстановлении NewPane', async () => {
+    getPaneById.mockReturnValue(secondaryPane);
+
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+      },
+      CompareMode.NewPane,
+      1,
+    );
+
+    expect(getPaneById).toHaveBeenCalledWith(1);
+    expect(addPane).not.toHaveBeenCalled();
+  });
+
+  it('не должен повторно добавлять одинаковый инструмент в одном режиме', async () => {
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+      },
+      CompareMode.Absolute,
+    );
+
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+      },
+      CompareMode.Absolute,
+    );
+
+    expect(addEntity).toHaveBeenCalledTimes(1);
+  });
+
+  it('не должен добавлять NewScale если compare уже существует', async () => {
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+      },
+      CompareMode.Absolute,
+    );
+
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'GAZP',
+      },
+      CompareMode.NewScale,
+    );
+
+    expect(addEntity).toHaveBeenCalledTimes(1);
+  });
+
+  it('должен включать левую шкалу для NewScale', async () => {
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+      },
+      CompareMode.NewScale,
+    );
+
+    expect(setPriceScaleSideVisible).toHaveBeenCalledWith(Direction.Left, true);
+  });
+
+  it('должен удалять compare-сущность', async () => {
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+      },
+      CompareMode.Absolute,
+    );
+
+    const [entry] = manager.getAllEntities();
+
+    if (!entry) {
+      throw new Error('Compare-сущность не была добавлена');
+    }
+
+    manager.removeSymbol('SBER');
+
+    expect(removeEntity).toHaveBeenCalledWith(entry.entity);
+    expect(entry.entity.destroy).toHaveBeenCalledTimes(1);
+    expect(manager.getAllEntities()).toHaveLength(0);
+  });
+
+  it('должен очищать все compare-сущности', async () => {
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'SBER',
+      },
+      CompareMode.Absolute,
+    );
+
+    await manager.setSymbolMode(
+      'Line' as SeriesType,
+      {
+        symbolId: 'GAZP',
+      },
+      CompareMode.NewPane,
+    );
+
+    expect(manager.getAllEntities()).toHaveLength(2);
+
+    manager.clear();
+
+    expect(manager.getAllEntities()).toHaveLength(0);
+    expect(removeEntity).toHaveBeenCalledTimes(2);
+  });
+});
diff --git a/src/core/__tests__/ContainerManager.test.ts b/src/core/__tests__/ContainerManager.test.ts
index 3c55fa4ebd21a5356f69f36848b776cd3d6eada4..9c548bf493d47f6bcde84da4c5c417afb87d50a6 100644
--- a/src/core/__tests__/ContainerManager.test.ts
+++ b/src/core/__tests__/ContainerManager.test.ts
@@ -1,6 +1,7 @@
 import {
   CHART_DRAWING_TOOLBAR_CONTAINER,
   CHART_MODAL_CONTAINER_CLASSNAME,
+  CHART_PANE_EMPTY_STATE_CONTAINER,
   CHART_PANE_OVERLAY_CONTAINER,
   CHART_ROOT_CLASSNAME,
 } from '@src/constants';
@@ -10,6 +11,7 @@ import { ContainerManager } from '../ContainerManager';
 jest.mock('@src/constants', () => ({
   CHART_DRAWING_TOOLBAR_CONTAINER: 'chart-drawing-toolbar-container',
   CHART_MODAL_CONTAINER_CLASSNAME: 'chart-modal-container',
+  CHART_PANE_EMPTY_STATE_CONTAINER: 'chart-pane-empty-state-container',
   CHART_PANE_OVERLAY_CONTAINER: 'chart-pane-overlay-container',
   CHART_ROOT_CLASSNAME: 'chart-root',
 }));
@@ -46,7 +48,6 @@ describe('ContainerManager', () => {
       });
 
       expect(parentContainer.classList.contains(CHART_ROOT_CLASSNAME)).toBe(true);
-
       expect(parentContainer.children[0]).toBe(headerContainer);
       expect(parentContainer.children[1]).toBe(chartContainer);
       expect(parentContainer.children[2]).toBe(footerContainer);
@@ -59,9 +60,7 @@ describe('ContainerManager', () => {
       expect(chartAreaContainer.contains(drawingToolbarContainer)).toBe(true);
 
       expect(drawingToolbarContainer.classList.contains(CHART_DRAWING_TOOLBAR_CONTAINER)).toBe(true);
-
       expect(modalContainer.classList.contains(CHART_MODAL_CONTAINER_CLASSNAME)).toBe(true);
-
       expect(modalContainer.hidden).toBe(true);
     });
 
@@ -121,9 +120,8 @@ describe('ContainerManager', () => {
       expect(chartContainer.style.gridTemplateColumns).toBe('42px minmax(0, 1fr)');
     });
 
-    it('должен удалять старое содержимое родительского контейнера перед созданием графика', () => {
+    it('должен удалять старое содержимое перед созданием контейнеров', () => {
       const oldElement = document.createElement('div');
-
       parentContainer.appendChild(oldElement);
 
       ContainerManager.createContainers({
@@ -148,48 +146,38 @@ describe('ContainerManager', () => {
       const fontLinks = document.querySelectorAll('link[href*="fonts.googleapis.com"]');
 
       expect(fontLinks).toHaveLength(1);
-      expect(fontLinks[0]?.getAttribute('href')).toBe(
-        'https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap',
-      );
     });
   });
 
   describe('createPaneContainers', () => {
-    it('должен создавать контейнеры легенды и оверлея панели', () => {
-      const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
+    it('должен создавать контейнеры pane с корректными настройками', () => {
+      const { legendContainer, paneOverlayContainer, paneEmptyStateContainer } =
+        ContainerManager.createPaneContainers();
 
       expect(legendContainer.style.position).toBe('absolute');
       expect(legendContainer.style.pointerEvents).toBe('none');
 
       expect(paneOverlayContainer.classList.contains(CHART_PANE_OVERLAY_CONTAINER)).toBe(true);
-
       expect(paneOverlayContainer.style.position).toBe('absolute');
+      expect(paneOverlayContainer.style.inset).toBe('0');
       expect(paneOverlayContainer.style.pointerEvents).toBe('none');
-    });
 
-    it('должен размещать слои графика в корректном порядке', () => {
-      const { controlBarContainer, drawingToolbarContainer, modalContainer } = ContainerManager.createContainers({
-        parentContainer,
-        showMenuButton: true,
-      });
-
-      const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
+      expect(paneEmptyStateContainer.classList.contains(CHART_PANE_EMPTY_STATE_CONTAINER)).toBe(true);
+      expect(paneEmptyStateContainer.hidden).toBe(true);
+    });
 
-      const controlBarZIndex = Number(controlBarContainer.style.zIndex);
-      const paneOverlayZIndex = Number(paneOverlayContainer.style.zIndex);
-      const drawingZIndex = Number(drawingToolbarContainer.style.zIndex);
-      const legendZIndex = Number(legendContainer.style.zIndex);
-      const modalZIndex = Number(modalContainer.style.zIndex);
+    it('должен создавать отдельные контейнеры для каждого pane', () => {
+      const first = ContainerManager.createPaneContainers();
+      const second = ContainerManager.createPaneContainers();
 
-      expect(controlBarZIndex).toBe(paneOverlayZIndex);
-      expect(legendZIndex).toBeGreaterThan(paneOverlayZIndex);
-      expect(drawingZIndex).toBeGreaterThan(legendZIndex);
-      expect(modalZIndex).toBeGreaterThan(drawingZIndex);
+      expect(first.legendContainer).not.toBe(second.legendContainer);
+      expect(first.paneOverlayContainer).not.toBe(second.paneOverlayContainer);
+      expect(first.paneEmptyStateContainer).not.toBe(second.paneEmptyStateContainer);
     });
   });
 
   describe('clearContainers', () => {
-    it('должен очищать содержимое родительского контейнера', () => {
+    it('должен очищать содержимое контейнера', () => {
       parentContainer.innerHTML = '<div>content</div>';
 
       ContainerManager.clearContainers(parentContainer);
diff --git a/src/core/__tests__/EventManager.test.ts b/src/core/__tests__/EventManager.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..779b6546147d2884ed6b2050a9f661d2b0e54876
--- /dev/null
+++ b/src/core/__tests__/EventManager.test.ts
@@ -0,0 +1,95 @@
+import { Timeframes } from '@src/types/timeframes';
+
+import { EventManager } from '../EventManager';
+
+describe('EventManager', () => {
+  function createEventManager(symbolId = ''): EventManager {
+    return new EventManager({
+      initialTimeframe: Timeframes['10s'],
+      initialSeries: 'Candlestick',
+      initialSymbolInfo: {
+        symbolId,
+        symbol: symbolId ? 'SBER' : '',
+        symbolName: symbolId ? 'Sberbank' : '',
+      },
+    });
+  }
+
+  it('должен поддерживать состояние без основного инструмента', () => {
+    const eventManager = createEventManager();
+    const hasSymbolValues: boolean[] = [];
+
+    const subscription = eventManager.hasSymbol().subscribe((hasSymbol) => {
+      hasSymbolValues.push(hasSymbol);
+    });
+
+    expect(hasSymbolValues).toEqual([false]);
+
+    let symbolId = 'initial';
+
+    const symbolSubscription = eventManager.symbolId().subscribe((value) => {
+      symbolId = value;
+    });
+
+    expect(symbolId).toBe('');
+
+    subscription.unsubscribe();
+    symbolSubscription.unsubscribe();
+    eventManager.destroy();
+  });
+
+  it('должен менять состояние наличия основного инструмента', () => {
+    const eventManager = createEventManager();
+    const values: boolean[] = [];
+
+    const subscription = eventManager.hasSymbol().subscribe((value) => {
+      values.push(value);
+    });
+
+    eventManager.setSymbol({
+      symbolId: 'SBER',
+      symbol: 'SBER',
+      symbolName: 'Sberbank',
+    });
+
+    eventManager.clearSymbol();
+
+    expect(values).toEqual([false, true, false]);
+    expect(eventManager.exportChartSettings().symbolInfo).toEqual({
+      symbolId: '',
+      symbol: '',
+      symbolName: '',
+    });
+
+    subscription.unsubscribe();
+    eventManager.destroy();
+  });
+
+  it('не должен изменять состояние при установке невалидного инструмента', () => {
+    const eventManager = createEventManager();
+
+    eventManager.setSymbol({
+      symbolId: '',
+    });
+
+    expect(eventManager.exportChartSettings().symbolInfo).toEqual({
+      symbolId: '',
+      symbol: '',
+      symbolName: '',
+    });
+
+    eventManager.destroy();
+  });
+
+  it('не должен повторно очищать уже пустой основной инструмент', () => {
+    const eventManager = createEventManager();
+    const undoRedo = eventManager.getUndoRedo();
+    const pushSpy = jest.spyOn(undoRedo, 'push');
+
+    eventManager.clearSymbol();
+
+    expect(pushSpy).not.toHaveBeenCalled();
+
+    eventManager.destroy();
+  });
+});
diff --git a/src/core/__tests__/IndicatorManager.test.ts b/src/core/__tests__/IndicatorManager.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4a2bcae02881a2252df5617dbe4db988816b4c57
--- /dev/null
+++ b/src/core/__tests__/IndicatorManager.test.ts
@@ -0,0 +1,191 @@
+import { IChartApi } from 'lightweight-charts';
+import { of } from 'rxjs';
+
+import { DataSource } from '@core/DataSource';
+import { DOMModel } from '@core/DOMModel';
+import { EventManager } from '@core/EventManager';
+import { Indicator } from '@core/Indicator';
+import { IndicatorManager } from '@core/IndicatorManager';
+import { Pane } from '@core/Pane';
+import { PaneManager } from '@core/PaneManager';
+
+import { IndicatorsIds } from '@src/constants';
+
+jest.mock('@core/Indicator');
+
+jest.mock('@src/core/Indicators', () => ({
+  indicatorsMap: () => ({
+    ema: {
+      newPane: false,
+      label: 'EMA',
+      series: [],
+    },
+  }),
+}));
+
+jest.mock('@src/utils', () => {
+  const actual = jest.requireActual<typeof import('@src/utils')>('@src/utils');
+
+  return {
+    ...actual,
+    applyNextIndicatorColors: (config: unknown) => config,
+    getIndicatorColors: () => [],
+  };
+});
+
+type EntityFactory = (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) => Indicator;
+
+describe('IndicatorManager', () => {
+  const IndicatorMock = Indicator as jest.MockedClass<typeof Indicator>;
+
+  let manager: IndicatorManager;
+  let mainPane: Pane;
+
+  let setEntity: jest.Mock;
+  let removeEntity: jest.Mock;
+  let getMainPane: jest.Mock;
+  let getPaneById: jest.Mock;
+  let addPane: jest.Mock;
+
+  beforeEach(() => {
+    IndicatorMock.mockImplementation(
+      (params) =>
+        ({
+          id: params.id,
+          destroy: jest.fn(),
+          getConfig: jest.fn(() => params.config),
+          getIndicatorType: jest.fn(() => params.type),
+        }) as unknown as Indicator,
+    );
+
+    mainPane = {
+      getId: jest.fn(() => 0),
+    } as unknown as Pane;
+
+    getMainPane = jest.fn(() => mainPane);
+    getPaneById = jest.fn();
+    addPane = jest.fn(() => mainPane);
+
+    setEntity = jest.fn((factory: EntityFactory) => factory(1, jest.fn(), jest.fn()));
+    removeEntity = jest.fn();
+
+    const paneManager = {
+      getMainPane,
+      getPaneById,
+      addPane,
+    } as unknown as PaneManager;
+
+    const eventManager = {
+      symbolId: jest.fn(() => of('SBER')),
+      symbol: jest.fn(() => of('SBER')),
+    } as unknown as EventManager;
+
+    const DOM = {
+      setEntity,
+      removeEntity,
+    } as unknown as DOMModel;
+
+    manager = new IndicatorManager({
+      eventManager,
+      dataSource: {} as DataSource,
+      lwcChart: {} as IChartApi,
+      paneManager,
+      DOM,
+    });
+  });
+
+  it('не должен добавлять индикатор без типа', () => {
+    const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
+
+    manager.addIndicator({});
+
+    expect(setEntity).not.toHaveBeenCalled();
+    expect(manager.getIndicators().value.size).toBe(0);
+
+    consoleSpy.mockRestore();
+  });
+
+  it('должен добавлять индикатор', () => {
+    manager.addIndicator({
+      id: 'ema-1',
+      indicatorType: IndicatorsIds.EMA,
+    });
+
+    expect(manager.getIndicators().value.has('ema-1')).toBe(true);
+    expect(getMainPane).toHaveBeenCalledTimes(1);
+    expect(IndicatorMock).toHaveBeenCalledTimes(1);
+  });
+
+  it('должен публиковать добавленные индикаторы', () => {
+    let entities: Indicator[] = [];
+
+    const subscription = manager.entities().subscribe((value) => {
+      entities = value;
+    });
+
+    manager.addIndicator({
+      id: 'ema-1',
+      indicatorType: IndicatorsIds.EMA,
+    });
+
+    expect(entities).toHaveLength(1);
+    expect(entities[0]).toBe(manager.getIndicators().value.get('ema-1'));
+
+    subscription.unsubscribe();
+  });
+
+  it('должен использовать pane из snapshot', () => {
+    const pane = {
+      getId: jest.fn(() => 3),
+    } as unknown as Pane;
+
+    getPaneById.mockReturnValue(pane);
+
+    manager.addIndicator({
+      id: 'ema-1',
+      indicatorType: IndicatorsIds.EMA,
+      paneId: 3,
+    });
+
+    expect(getPaneById).toHaveBeenCalledWith(3);
+    expect(addPane).not.toHaveBeenCalled();
+  });
+
+  it('должен создать pane если pane из snapshot отсутствует', () => {
+    getPaneById.mockReturnValue(undefined);
+
+    manager.addIndicator({
+      id: 'ema-1',
+      indicatorType: IndicatorsIds.EMA,
+      paneId: 3,
+    });
+
+    expect(getPaneById).toHaveBeenCalledWith(3);
+    expect(addPane).toHaveBeenCalledTimes(1);
+  });
+
+  it('должен удалять индикатор из состояния и DOM', () => {
+    manager.addIndicator({
+      id: 'ema-1',
+      indicatorType: IndicatorsIds.EMA,
+    });
+
+    const indicator = manager.getIndicators().value.get('ema-1');
+
+    if (!indicator) {
+      throw new Error('Индикатор не был добавлен');
+    }
+
+    manager.deleteIndicator('ema-1');
+
+    expect(manager.getIndicators().value.has('ema-1')).toBe(false);
+    expect(removeEntity).toHaveBeenCalledWith(indicator);
+    expect(indicator.destroy).toHaveBeenCalledTimes(1);
+  });
+
+  it('не должен удалять несуществующий индикатор', () => {
+    manager.deleteIndicator('unknown');
+
+    expect(removeEntity).not.toHaveBeenCalled();
+  });
+});
diff --git a/src/core/__tests__/MoexChart.test.ts b/src/core/__tests__/MoexChart.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..390c5fd405f585cf8e27f1dcad59c106f628c4fe
--- /dev/null
+++ b/src/core/__tests__/MoexChart.test.ts
@@ -0,0 +1,39 @@
+import { MoexChart } from '@core/MoexChart';
+
+describe('MoexChart', () => {
+  it('должен очищать основной инструмент через EventManager', () => {
+    const clearSymbol = jest.fn();
+
+    const chart = Object.create(MoexChart.prototype) as MoexChart;
+
+    (chart as unknown as { eventManager: { clearSymbol: () => void } }).eventManager = {
+      clearSymbol,
+    };
+
+    chart.clearSymbol();
+
+    expect(clearSymbol).toHaveBeenCalledTimes(1);
+  });
+
+  it('должен устанавливать основной инструмент через EventManager', () => {
+    const setSymbol = jest.fn();
+
+    const chart = Object.create(MoexChart.prototype) as MoexChart;
+
+    (chart as unknown as { eventManager: { setSymbol: typeof setSymbol } }).eventManager = {
+      setSymbol,
+    };
+
+    chart.setSymbol({
+      symbolId: 'SBER',
+      symbol: 'SBER',
+      symbolName: 'Sberbank',
+    });
+
+    expect(setSymbol).toHaveBeenCalledWith({
+      symbolId: 'SBER',
+      symbol: 'SBER',
+      symbolName: 'Sberbank',
+    });
+  });
+});
diff --git a/src/core/__tests__/Pane.test.ts b/src/core/__tests__/Pane.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d425fb524776ca1157d81e679d9f23f978d1ffd2
--- /dev/null
+++ b/src/core/__tests__/Pane.test.ts
@@ -0,0 +1,225 @@
+import { BehaviorSubject, of } from 'rxjs';
+
+import { ContainerManager } from '@core/ContainerManager';
+import { DrawingsManager } from '@core/DrawingsManager';
+import { Indicator } from '@core/Indicator';
+import { Legend } from '@core/Legend';
+import { Pane } from '@core/Pane';
+import { PriceScale, PriceScaleControls } from '@core/PriceScale';
+import { ReactRenderer } from '@core/ReactRenderer';
+import { TooltipService } from '@core/Tooltip';
+
+import { IndicatorsIds } from '@src/constants';
+
+jest.mock('@core/ContainerManager');
+jest.mock('@core/DrawingsManager');
+jest.mock('@core/Legend');
+jest.mock('@core/PriceScale');
+jest.mock('@core/ReactRenderer');
+jest.mock('@core/Tooltip');
+
+jest.mock('@src/translations', () => {
+  const actual = jest.requireActual<typeof import('@src/translations')>('@src/translations');
+
+  return {
+    ...actual,
+    t: (key: string) => key,
+  };
+});
+
+describe('Pane', () => {
+  const createPaneContainersMock = ContainerManager.createPaneContainers as jest.MockedFunction<
+    typeof ContainerManager.createPaneContainers
+  >;
+
+  let hasSymbol$: BehaviorSubject<boolean>;
+  let paneElement: HTMLTableRowElement;
+  let emptyStateContainer: HTMLDivElement;
+  let onDelete: jest.Mock;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    Object.defineProperty(global, 'requestAnimationFrame', {
+      writable: true,
+      value: (callback: FrameRequestCallback) => {
+        callback(0);
+        return 1;
+      },
+    });
+
+    Object.defineProperty(global, 'cancelAnimationFrame', {
+      writable: true,
+      value: jest.fn(),
+    });
+
+    emptyStateContainer = document.createElement('div');
+    emptyStateContainer.hidden = true;
+
+    createPaneContainersMock.mockReturnValue({
+      legendContainer: document.createElement('div'),
+      paneOverlayContainer: document.createElement('div'),
+      paneEmptyStateContainer: emptyStateContainer,
+    });
+
+    (ReactRenderer as jest.MockedClass<typeof ReactRenderer>).mockImplementation(
+      () =>
+        ({
+          renderComponent: jest.fn(),
+          destroy: jest.fn(),
+        }) as unknown as ReactRenderer,
+    );
+
+    (Legend as jest.MockedClass<typeof Legend>).mockImplementation(
+      () =>
+        ({
+          getConfig: jest.fn(() => ({})),
+          getLegendViewModel: jest.fn(() => of([])),
+          destroy: jest.fn(),
+        }) as unknown as Legend,
+    );
+
+    (TooltipService as jest.MockedClass<typeof TooltipService>).mockImplementation(
+      () =>
+        ({
+          getTooltipViewModel: jest.fn(() => of({})),
+          getConfig: jest.fn(() => ({})),
+          destroy: jest.fn(),
+        }) as unknown as TooltipService,
+    );
+
+    (PriceScale as jest.MockedClass<typeof PriceScale>).mockImplementation(
+      () =>
+        ({
+          getSnapshot: jest.fn(() => ({})),
+          enableAutoScale: jest.fn(),
+        }) as unknown as PriceScale,
+    );
+
+    (PriceScaleControls as jest.MockedClass<typeof PriceScaleControls>).mockImplementation(
+      () =>
+        ({
+          refresh: jest.fn(),
+          mount: jest.fn(),
+          destroy: jest.fn(),
+        }) as unknown as PriceScaleControls,
+    );
+
+    (DrawingsManager as jest.MockedClass<typeof DrawingsManager>).mockImplementation(
+      () =>
+        ({
+          entities: jest.fn(() => of([])),
+          destroy: jest.fn(),
+          getSnapshot: jest.fn(() => []),
+          setSnapshot: jest.fn(),
+        }) as unknown as DrawingsManager,
+    );
+
+    hasSymbol$ = new BehaviorSubject(false);
+    onDelete = jest.fn();
+
+    paneElement = document.createElement('tr');
+    paneElement.append(document.createElement('td'), document.createElement('td'), document.createElement('td'));
+  });
+
+  function createPane(): Pane {
+    const mainSeries$ = new BehaviorSubject(null);
+
+    return new Pane({
+      id: 1,
+      isMainPane: false,
+      lwcChart: {
+        addPane: jest.fn(() => ({
+          getHTMLElement: jest.fn(() => paneElement),
+          paneIndex: jest.fn(() => 1),
+        })),
+      } as never,
+      eventManager: {
+        hasSymbol: jest.fn(() => hasSymbol$),
+        getChartOptionsModel: jest.fn(() => of({})),
+        getTimeframeObs: jest.fn(() => of('10s')),
+      } as never,
+      DOM: {} as never,
+      ohlcConfig: {} as never,
+      dataSource: null,
+      basedOn: {
+        getMainSerie: jest.fn(() => mainSeries$),
+      } as never,
+      subscribeChartEvent: jest.fn() as never,
+      tooltipConfig: {},
+      onDelete,
+      chartContainer: document.createElement('div'),
+      modalRenderer: {} as never,
+      onPriceScaleStateChange: jest.fn(),
+      leftPriceScaleVisible: false,
+      rightPriceScaleVisible: true,
+      hotkeys: {} as never,
+      addDrawingManager: jest.fn(),
+      removeDrawingManager: jest.fn(),
+      setActiveTool: jest.fn(),
+      getActiveTool: jest.fn(),
+      getIsEndlessMode: jest.fn(),
+      continueDrawing: jest.fn(),
+    });
+  }
+
+  it('должен показывать empty state для индикатора без основного инструмента', () => {
+    const pane = createPane();
+
+    const indicator = {
+      getIndicatorType: jest.fn(() => IndicatorsIds.RSI),
+      recreateSeries: jest.fn(),
+    } as unknown as Indicator;
+
+    pane.setIndicator('rsi', indicator);
+
+    expect(emptyStateContainer.hidden).toBe(false);
+
+    pane.destroy();
+  });
+
+  it('должен скрывать empty state после добавления основного инструмента', () => {
+    const pane = createPane();
+
+    pane.setIndicator('rsi', {
+      getIndicatorType: jest.fn(() => IndicatorsIds.RSI),
+      recreateSeries: jest.fn(),
+    } as unknown as Indicator);
+
+    expect(emptyStateContainer.hidden).toBe(false);
+
+    hasSymbol$.next(true);
+
+    expect(emptyStateContainer.hidden).toBe(true);
+
+    pane.destroy();
+  });
+
+  it('не должен показывать empty state только для compare-сущности', () => {
+    const pane = createPane();
+
+    pane.setIndicator('compare', {
+      getIndicatorType: jest.fn(() => undefined),
+      recreateSeries: jest.fn(),
+    } as unknown as Indicator);
+
+    expect(emptyStateContainer.hidden).toBe(true);
+
+    pane.destroy();
+  });
+
+  it('должен удалить secondary pane после удаления последнего индикатора', () => {
+    const pane = createPane();
+
+    pane.setIndicator('rsi', {
+      getIndicatorType: jest.fn(() => IndicatorsIds.RSI),
+      recreateSeries: jest.fn(),
+    } as unknown as Indicator);
+
+    pane.removeIndicator('rsi');
+
+    expect(onDelete).toHaveBeenCalledTimes(1);
+
+    pane.destroy();
+  });
+});
diff --git a/src/core/__tests__/PaneManager.test.ts b/src/core/__tests__/PaneManager.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..79adee94c2eee2d7958599a338f0ce1f4a08b7e9
--- /dev/null
+++ b/src/core/__tests__/PaneManager.test.ts
@@ -0,0 +1,128 @@
+import { PriceScaleMode } from 'lightweight-charts';
+import { BehaviorSubject, of } from 'rxjs';
+
+import { DrawingsManagerCollection } from '@core/DrawingsManagerCollection';
+import { Pane } from '@core/Pane';
+import { PaneManager } from '@core/PaneManager';
+import { PriceAxisLabels } from '@core/PriceAxisLabels';
+
+import { IndicatorsIds } from '@src/constants';
+import { PaneSnapshot } from '@src/types/snapshot';
+
+jest.mock('@core/Pane');
+jest.mock('@core/DrawingsManagerCollection');
+jest.mock('@core/PriceAxisLabels');
+
+describe('PaneManager', () => {
+  const PaneMock = Pane as jest.MockedClass<typeof Pane>;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    PaneMock.mockImplementation(
+      (params) =>
+        ({
+          getId: jest.fn(() => params.id),
+          paneIndex: jest.fn(() => params.id),
+          getMainSerie: jest.fn(() => new BehaviorSubject(null)),
+          setDrawingsSnapshot: jest.fn(),
+          getDrawingsSnapshot: jest.fn(() => []),
+          schedulePaneContainerSync: jest.fn(),
+          refreshPriceScaleControls: jest.fn(),
+          resetPriceScalesAutoScale: jest.fn(),
+          destroy: jest.fn(),
+          getSnapshot: jest.fn(() => ({
+            isMain: params.isMainPane,
+            id: params.id,
+            indicators: [],
+            drawings: [],
+          })),
+          getPriceScale: jest.fn(() => ({
+            getMode: jest.fn(() => PriceScaleMode.Normal),
+            setVisible: jest.fn(),
+          })),
+        }) as unknown as Pane,
+    );
+
+    (DrawingsManagerCollection as jest.MockedClass<typeof DrawingsManagerCollection>).mockImplementation(
+      () =>
+        ({
+          destroy: jest.fn(),
+          handlePaneClick: jest.fn(),
+          addDrawingManager: jest.fn(),
+          removeDrawingManager: jest.fn(),
+          setActiveTool: jest.fn(),
+          getActiveToolValue: jest.fn(),
+          getIsEndlessMode: jest.fn(),
+          activateDrawingTool: jest.fn(),
+        }) as unknown as DrawingsManagerCollection,
+    );
+
+    (PriceAxisLabels as jest.MockedClass<typeof PriceAxisLabels>).mockImplementation(
+      () =>
+        ({
+          destroy: jest.fn(),
+          invalidate: jest.fn(),
+          setVisibleLogicalRange: jest.fn(),
+        }) as unknown as PriceAxisLabels,
+    );
+  });
+
+  function createManager(panesSnapshot: PaneSnapshot[]): PaneManager {
+    const params = {
+      panesSnapshot,
+      lwcChart: {
+        subscribeClick: jest.fn(),
+        unsubscribeClick: jest.fn(),
+        removePane: jest.fn(),
+      },
+      eventManager: {
+        symbol: jest.fn(() => of('')),
+      },
+      dataSource: null,
+      DOM: {},
+      ohlcConfig: {},
+      subscribeChartEvent: jest.fn(),
+      tooltipConfig: {},
+      chartContainer: document.createElement('div'),
+      modalRenderer: {},
+      hotkeys: {},
+    } as unknown as ConstructorParameters<typeof PaneManager>[0];
+
+    return new PaneManager(params);
+  }
+
+  it('не должен восстанавливать пустой secondary pane', () => {
+    const manager = createManager([
+      {
+        isMain: true,
+        id: 0,
+        indicators: [],
+        drawings: [],
+      },
+      {
+        isMain: false,
+        id: 1,
+        indicators: [],
+        drawings: [],
+      },
+      {
+        isMain: false,
+        id: 2,
+        indicators: [
+          {
+            indicatorType: IndicatorsIds.RSI,
+          },
+        ],
+        drawings: [],
+      },
+    ]);
+
+    expect(manager.getPanes().has(0)).toBe(true);
+    expect(manager.getPanes().has(1)).toBe(false);
+    expect(manager.getPanes().has(2)).toBe(true);
+    expect(manager.getPanes().size).toBe(2);
+
+    manager.destroy();
+  });
+});
diff --git a/src/core/__tests__/ReactRenderer.test.ts b/src/core/__tests__/ReactRenderer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..35441bfd06e6a4e19ca271588f2cae7b62bca071
--- /dev/null
+++ b/src/core/__tests__/ReactRenderer.test.ts
@@ -0,0 +1,144 @@
+import React from 'react';
+import { createRoot, Root } from 'react-dom/client';
+
+import { ReactRenderer } from '../ReactRenderer';
+
+jest.mock('react-dom/client', () => ({
+  createRoot: jest.fn(),
+}));
+
+describe('ReactRenderer', () => {
+  const createRootMock = createRoot as jest.MockedFunction<typeof createRoot>;
+
+  let container: HTMLDivElement;
+  let root: jest.Mocked<Pick<Root, 'render' | 'unmount'>>;
+  let requestAnimationFrameSpy: jest.SpyInstance;
+
+  beforeEach(() => {
+    container = document.createElement('div');
+
+    root = {
+      render: jest.fn(),
+      unmount: jest.fn(),
+    };
+
+    createRootMock.mockReturnValue(root as unknown as Root);
+
+    requestAnimationFrameSpy = jest.spyOn(global, 'requestAnimationFrame').mockImplementation((callback) => {
+      callback(0);
+
+      return 1;
+    });
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+    requestAnimationFrameSpy.mockRestore();
+  });
+
+  it('не должен создавать React root до первого рендера', () => {
+    const renderer = new ReactRenderer(container);
+
+    expect(renderer.isInitialized()).toBe(false);
+    expect(createRootMock).not.toHaveBeenCalled();
+  });
+
+  it('должен создавать React root при первом рендере', () => {
+    const renderer = new ReactRenderer(container);
+    const component = React.createElement('div', null, 'content');
+
+    renderer.render(component);
+
+    expect(createRootMock).toHaveBeenCalledTimes(1);
+    expect(createRootMock).toHaveBeenCalledWith(container);
+    expect(root.render).toHaveBeenCalledWith(component);
+    expect(renderer.isInitialized()).toBe(true);
+  });
+
+  it('не должен повторно создавать React root при последующих рендерах', () => {
+    const renderer = new ReactRenderer(container);
+
+    const firstComponent = React.createElement('div', null, 'first');
+    const secondComponent = React.createElement('div', null, 'second');
+
+    renderer.render(firstComponent);
+    renderer.render(secondComponent);
+
+    expect(createRootMock).toHaveBeenCalledTimes(1);
+    expect(root.render).toHaveBeenCalledTimes(2);
+    expect(root.render).toHaveBeenNthCalledWith(1, firstComponent);
+    expect(root.render).toHaveBeenNthCalledWith(2, secondComponent);
+  });
+
+  it('должен делегировать renderComponent в render', () => {
+    const renderer = new ReactRenderer(container);
+    const component = React.createElement('div', null, 'content');
+
+    renderer.renderComponent(component);
+
+    expect(createRootMock).toHaveBeenCalledTimes(1);
+    expect(root.render).toHaveBeenCalledWith(component);
+  });
+
+  it('должен очищать React root', () => {
+    const renderer = new ReactRenderer(container);
+
+    renderer.render(React.createElement('div'));
+
+    expect(renderer.isInitialized()).toBe(true);
+
+    renderer.clear();
+
+    expect(renderer.isInitialized()).toBe(false);
+    expect(root.unmount).toHaveBeenCalledTimes(1);
+  });
+
+  it('не должен выполнять unmount если renderer ещё не инициализирован', () => {
+    const renderer = new ReactRenderer(container);
+
+    renderer.clear();
+
+    expect(root.unmount).not.toHaveBeenCalled();
+    expect(renderer.isInitialized()).toBe(false);
+  });
+
+  it('должен очищаться через clearComponents', () => {
+    const renderer = new ReactRenderer(container);
+
+    renderer.render(React.createElement('div'));
+    renderer.clearComponents();
+
+    expect(renderer.isInitialized()).toBe(false);
+    expect(root.unmount).toHaveBeenCalledTimes(1);
+  });
+
+  it('должен очищаться при destroy', () => {
+    const renderer = new ReactRenderer(container);
+
+    renderer.render(React.createElement('div'));
+    renderer.destroy();
+
+    expect(renderer.isInitialized()).toBe(false);
+    expect(root.unmount).toHaveBeenCalledTimes(1);
+  });
+
+  it('должен позволять повторную инициализацию после clear', () => {
+    const renderer = new ReactRenderer(container);
+
+    renderer.render(React.createElement('div', null, 'first'));
+    renderer.clear();
+
+    const secondRoot = {
+      render: jest.fn(),
+      unmount: jest.fn(),
+    };
+
+    createRootMock.mockReturnValue(secondRoot as unknown as Root);
+
+    renderer.render(React.createElement('div', null, 'second'));
+
+    expect(createRootMock).toHaveBeenCalledTimes(2);
+    expect(secondRoot.render).toHaveBeenCalledTimes(1);
+    expect(renderer.isInitialized()).toBe(true);
+  });
+});
diff --git a/src/core/__tests__/TimescaleHoverController.test.ts b/src/core/__tests__/TimescaleHoverController.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0737823d99bbe793f99674f46342d93b8a457b9d
--- /dev/null
+++ b/src/core/__tests__/TimescaleHoverController.test.ts
@@ -0,0 +1,81 @@
+import { EventManager } from '@core/EventManager';
+import { TimeScaleHoverController } from '@core/TimescaleHoverController';
+
+describe('TimeScaleHoverController', () => {
+  let chartContainer: HTMLDivElement;
+  let controlBarContainer: HTMLDivElement;
+  let eventManager: jest.Mocked<Pick<EventManager, 'setControlBarVisible'>>;
+
+  beforeEach(() => {
+    chartContainer = document.createElement('div');
+    controlBarContainer = document.createElement('div');
+
+    jest.spyOn(chartContainer, 'getBoundingClientRect').mockReturnValue({
+      x: 0,
+      y: 0,
+      top: 0,
+      left: 0,
+      right: 1000,
+      bottom: 500,
+      width: 1000,
+      height: 500,
+      toJSON: jest.fn(),
+    });
+
+    eventManager = {
+      setControlBarVisible: jest.fn(),
+    };
+  });
+
+  it('должен показывать ControlBar при наведении на нижнюю центральную область', () => {
+    const controller = new TimeScaleHoverController({
+      eventManager: eventManager as unknown as EventManager,
+      chartContainer,
+      controlBarContainer,
+    });
+
+    chartContainer.dispatchEvent(
+      new MouseEvent('mousemove', {
+        clientX: 500,
+        clientY: 480,
+      }),
+    );
+
+    expect(eventManager.setControlBarVisible).toHaveBeenLastCalledWith(true);
+
+    controller.destroy();
+  });
+
+  it('должен скрывать ControlBar вне активной области', () => {
+    const controller = new TimeScaleHoverController({
+      eventManager: eventManager as unknown as EventManager,
+      chartContainer,
+      controlBarContainer,
+    });
+
+    chartContainer.dispatchEvent(
+      new MouseEvent('mousemove', {
+        clientX: 100,
+        clientY: 100,
+      }),
+    );
+
+    expect(eventManager.setControlBarVisible).toHaveBeenLastCalledWith(false);
+
+    controller.destroy();
+  });
+
+  it('должен показывать ControlBar при наведении на него', () => {
+    const controller = new TimeScaleHoverController({
+      eventManager: eventManager as unknown as EventManager,
+      chartContainer,
+      controlBarContainer,
+    });
+
+    controlBarContainer.dispatchEvent(new MouseEvent('mouseenter'));
+
+    expect(eventManager.setControlBarVisible).toHaveBeenLastCalledWith(true);
+
+    controller.destroy();
+  });
+});
diff --git a/src/core/styles.module.scss b/src/core/styles.module.scss
deleted file mode 100644
index 685cbe25d7b51cc64a0c94cc11893cdca7ae91f1..0000000000000000000000000000000000000000
--- a/src/core/styles.module.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@use '../theme/mixins' as m;
-
-.scrollableBox {
-  @include m.scrollbar;
-}
-
-.safariFullscreen {
-  position: fixed;
-  top: 0;
-  left: 0;
-  width: 100vw;
-  height: 100vh;
-  z-index: 9999;
-}
diff --git a/src/styles/global.scss b/src/styles/global.scss
index ac4bd31a770cfd0c8cee0c490fbe2e9afa8fb0ae..1d8f87357a7d5fab4272ce9e144d4735fc7bee13 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -1,3 +1,4 @@
+@use '../theme/mixins' as m;
 @use './preflight';
 
 .moex-chart-root {
@@ -41,3 +42,34 @@
     }
   }
 }
+
+.moex-chart-pane-empty-state-container {
+  position: absolute;
+  inset: 0;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: var(--space-1000);
+  font-size: var(--space-0875);
+  color: var(--neutral-12);
+  text-align: center;
+  pointer-events: none;
+
+  &[hidden] {
+    display: none;
+  }
+}
+
+.moex-chart-fullscreen-safari {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 9999;
+}
+
+.moex-chart-scrollable {
+  @include m.scrollbar;
+}
diff --git a/src/translations/russianDict.ts b/src/translations/russianDict.ts
index df1e548e509953a6239008a4bc75897e75aedcac..1aca52bcf98e528d205e53fef7dd0edbc42e3eb6 100644
--- a/src/translations/russianDict.ts
+++ b/src/translations/russianDict.ts
@@ -222,4 +222,6 @@ export const russian = {
 
   '1М': '1M',
   '3М': '3M',
+
+  'Add a main instrument to display indicators': 'Добавьте основной инструмент для отображения индикаторов',
 };
diff --git a/src/types/snapshot.ts b/src/types/snapshot.ts
index 93ee59bb280a0b6c45af8973cee1301b298c8f89..1da50d49e541b0cea7e6544aaffdb0433597fab6 100644
--- a/src/types/snapshot.ts
+++ b/src/types/snapshot.ts
@@ -1,7 +1,7 @@
 import { DrawingsManagerSnapshot } from '@core/DrawingsManager';
 import { ChartSeriesType, DateFormat, IndicatorsIds, Intervals, Timeframes } from '@lib';
 import { Direction } from '@src/types/chart';
-import { SymbolInfo, SymbolInfoInput } from '@src/types/symbol';
+import { MainSymbolSnapshotInput, SymbolInfo, SymbolInfoInput } from '@src/types/symbol';
 import { TimeFormat } from '@src/types/timeScale';
 
 import { SettingsValues } from './settings';
@@ -14,7 +14,7 @@ export interface ISerializable<T extends object> {
   getSnapshot: () => T;
 }
 
-export interface InitialSnapshot extends SymbolInfoInput {
+export interface InitialSnapshot extends MainSymbolSnapshotInput {
   timeframe: Timeframes; // todo: move to snap
   chartSeriesType: ChartSeriesType; // todo: move to snap
 }
@@ -38,7 +38,7 @@ interface ChartSnapshotBase {
   panes: PaneSnapshot[];
 }
 
-export interface ChartSnapshotInput extends ChartSnapshotBase, SymbolInfoInput {}
+export interface ChartSnapshotInput extends ChartSnapshotBase, MainSymbolSnapshotInput {}
 export interface ChartSnapshot extends ChartSnapshotBase, SymbolInfo {}
 
 export interface PriceScaleSnapshot {
diff --git a/src/types/symbol.ts b/src/types/symbol.ts
index a79793447aea2198711e265c7c76975c4a7fce62..92a0bcd3df7a147562be8621bbc954f0235fb9a4 100644
--- a/src/types/symbol.ts
+++ b/src/types/symbol.ts
@@ -9,3 +9,7 @@ export interface SymbolInfoInput {
   symbol?: string;
   symbolName?: string;
 }
+
+export interface MainSymbolSnapshotInput extends Omit<SymbolInfoInput, 'symbolId'> {
+  symbolId: string | null;
+}