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


diff --git a/.gitignore b/.gitignore
index 81032cd0664cf01d0646f2decaf1f8b0627f6d1b..8f371ae3895902342be8db77a1bca3fc876e774a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ storybook-static
 yalc.lock
 coverage/
 .scannerwork/
+.kilo/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ced559768fe9af635167c13cacfc74c73b5cfe4..919ca48e60dda72239cb92ca105c8c7477ef70ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
 - Добавлены тултипы для элементов управления в шапке графика
 - Добавлен параметр `showSnapshotButton?: boolean` для управления отображением кнопки «Снимок графика»
 - Добавлен VWAP индикатор
+- Добавлена механика ctrl+c/ctrl+v для дровингов
 
 # 0.2.1
 
diff --git a/src/components/Loader/index.module.scss b/src/components/Loader/index.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..6cd13ece627595713578598758e1dac98a380636
--- /dev/null
+++ b/src/components/Loader/index.module.scss
@@ -0,0 +1,28 @@
+.loaderWrapper {
+  position: 'relative';
+  width: '100%';
+  height: '100%';
+  background-color: '#fff';
+  z-index: '100';
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.loader {
+  border: 10px solid #1b1717;
+  border-top: 10px solid #242627;
+  border-radius: 50%;
+  width: 120px;
+  height: 120px;
+  animation: spin 2s linear infinite;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..89c48f1cce9882665e5845f11cd05ec6617d4594
--- /dev/null
+++ b/src/components/Loader/index.tsx
@@ -0,0 +1,12 @@
+import styles from './index.module.scss';
+
+export const Loader = () => {
+  return (
+    <div
+      className={styles.loaderWrapper}
+      style={{ position: 'relative', width: '100%', height: '100%', backgroundColor: 'transparent', zIndex: '100' }}
+    >
+      <div className={styles.loader} />
+    </div>
+  );
+};
diff --git a/src/core/ContainerManager.ts b/src/core/ContainerManager.ts
index 3f7411d0a68145bb732104700ed34f3547550be6..538b7209b46fed4ba9dbae018ac79177914d0379 100644
--- a/src/core/ContainerManager.ts
+++ b/src/core/ContainerManager.ts
@@ -155,6 +155,15 @@ export class ContainerManager {
       minHeight: '0',
     });
 
+    const loaderContainer = document.createElement('div');
+    loaderContainer.style.position = 'absolute';
+    loaderContainer.style.height = '100%';
+    loaderContainer.style.width = '100%';
+    loaderContainer.style.minHeight = '0';
+    loaderContainer.style.minWidth = '0';
+    loaderContainer.style.outline = 'none';
+    loaderContainer.tabIndex = -1;
+
     const toolBarContainer = document.createElement('div');
 
     Object.assign(toolBarContainer.style, {
@@ -195,7 +204,7 @@ export class ContainerManager {
     });
 
     seasonalityContainer.append(seasonalityControlsContainer, seasonalityPlotContainer);
-    chartAreaContainer.append(controlBarContainer, drawingToolbarContainer);
+    chartAreaContainer.append(controlBarContainer, drawingToolbarContainer, loaderContainer);
     chartContainer.append(chartAreaContainer, modalContainer);
     chartViewContainer.append(chartContainer, seasonalityContainer);
     parentContainer.append(headerContainer, chartViewContainer);
@@ -240,6 +249,7 @@ export class ContainerManager {
       modalContainer,
       controlBarContainer,
       drawingToolbarContainer,
+      loaderContainer,
       toggleToolbar,
     };
   }
diff --git a/src/core/DataSource.ts b/src/core/DataSource.ts
index 1017342b5907e89a3aac7ce33923d63bfc47ad1c..adcdca0e017deda7f635ab99f608e1266f30340f 100644
--- a/src/core/DataSource.ts
+++ b/src/core/DataSource.ts
@@ -1,5 +1,5 @@
-import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs';
-import { map } from 'rxjs/operators';
+import { BehaviorSubject, combineLatest, Observable, of, Subject, Subscription } from 'rxjs';
+import { map, switchMap } from 'rxjs/operators';
 
 import { EventManager } from '@core/EventManager';
 import { SymbolSource } from '@src/core/SymbolSource';
@@ -26,6 +26,7 @@ export class DataSource {
   private readonly eventManager: EventManager;
 
   private readonly states = new Map<string, SymbolSource>();
+  private readonly activeStates$ = new BehaviorSubject<Set<SymbolSource>>(new Set());
   private readonly activeSymbols$ = new BehaviorSubject<Set<string>>(new Set());
 
   private realtimeSub: Subscription | null = null;
@@ -37,6 +38,23 @@ export class DataSource {
     this.initSubscriptions();
   }
 
+  public isLoading = (): Observable<boolean> => {
+    return this.activeStates$.pipe(
+      switchMap((activeStates: Set<SymbolSource>) => {
+        if (activeStates.size === 0) {
+          return of(false);
+        }
+
+        const isSymbolLoading$ = Array.from(activeStates).map((st) => st.isLoading());
+        return combineLatest(isSymbolLoading$).pipe(
+          map((isSymbolLoading) => {
+            return isSymbolLoading.some((val) => val === true);
+          }),
+        );
+      }),
+    );
+  };
+
   public setSymbols(symbols: string[]): void {
     const nextSet = new Set<string>();
 
@@ -175,6 +193,17 @@ export class DataSource {
   }
 
   private initSubscriptions(): void {
+    this.subscriptions.add(
+      this.activeSymbols$.subscribe((symbolsSet) => {
+        const symbolSources: Set<SymbolSource> = new Set();
+        [...symbolsSet].forEach((symbol) => {
+          symbolSources.add(this.ensureState(symbol));
+        });
+
+        this.activeStates$.next(symbolSources);
+      }),
+    );
+
     this.subscriptions.add(
       combineLatest([this.eventManager.getSelectedSeries()]).subscribe(() => {
         this.states.forEach((st) => st.saveRealtimeCache());
diff --git a/src/core/Drawings.ts b/src/core/Drawings.ts
index dc72e4e6bddef066e1e1907753944e365166c2b6..d80060268388852b2acd22de541901861ea4354a 100644
--- a/src/core/Drawings.ts
+++ b/src/core/Drawings.ts
@@ -15,7 +15,6 @@ interface DrawingParams extends DOMObjectParams {
   lwcChart: IChartApi;
   mainSeries: SeriesStrategies;
   onDelete: (id: string) => void;
-  onCopy: () => void;
   construct: (chart: IChartApi, series: ISeriesApi<SeriesType>, interaction: DrawingInteraction) => ISeriesDrawing;
   selected$: Observable<boolean>;
   isSelected: () => boolean;
@@ -35,7 +34,6 @@ export class Drawing extends DOMObject {
   private subscriptions = new Subscription();
 
   private unregisterDeleteHotkey = () => {};
-  private unregisterCopyHotkey = () => {};
 
   constructor({
     lwcChart,
@@ -44,7 +42,6 @@ export class Drawing extends DOMObject {
     drawingName,
     id,
     onDelete,
-    onCopy,
     zIndex,
     moveUp,
     moveDown,
@@ -103,15 +100,6 @@ export class Drawing extends DOMObject {
             this.delete();
           },
         });
-
-        this.unregisterCopyHotkey = this.hotkeys.register({
-          keys: [Keys.mod, Keys.c],
-          callback: () => {
-            if (!this.isCreationPending()) {
-              onCopy();
-            }
-          },
-        });
       }),
     );
   }
@@ -219,9 +207,7 @@ export class Drawing extends DOMObject {
 
   private unregisterSelectedDrawingHotkeys(): void {
     this.unregisterDeleteHotkey();
-    this.unregisterCopyHotkey();
 
     this.unregisterDeleteHotkey = () => {};
-    this.unregisterCopyHotkey = () => {};
   }
 }
diff --git a/src/core/Drawings/axisLine/axisLine.ts b/src/core/Drawings/axisLine/axisLine.ts
index 9da496e9abda866ef0c076e722b784b3433974e3..8bce3938d26a20a7ddfa9640033e2fc3a72cf043 100644
--- a/src/core/Drawings/axisLine/axisLine.ts
+++ b/src/core/Drawings/axisLine/axisLine.ts
@@ -36,6 +36,19 @@ interface AxisLineParams extends BaseDrawingParams {
   direction: AxisLineDirection;
 }
 
+export function isAxisLineState(state: unknown): state is AxisLineState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'time' in state &&
+    'price' in state &&
+    ((typeof state.time === 'number' && state.price === null) ||
+      (state.time === null && typeof state.price === 'number'))
+  );
+}
+
 interface AxisLineState {
   hidden: boolean;
   mode: AxisLineMode;
diff --git a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
index a288a14cad591c7118567b6e2718e3ac59e7ea6b..ccc8e3b691698e1783ad044b1fc162651b56d9e1 100644
--- a/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
+++ b/src/core/Drawings/fibonacciRetracement/fibonacciRetracement.ts
@@ -29,6 +29,23 @@ type PriceLabelKind = 'top' | 'bottom';
 
 type FibonacciRetracementParams = BaseDrawingParams;
 
+export function isFibonacciRetracementState(state: unknown): state is FibonacciRetracementState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'startTime' in state &&
+    typeof state.startTime === 'number' &&
+    'endTime' in state &&
+    typeof state.endTime === 'number' &&
+    'startPrice' in state &&
+    typeof state.startPrice === 'number' &&
+    'endPrice' in state &&
+    typeof state.endPrice === 'number'
+  );
+}
+
 interface FibonacciRetracementState {
   hidden: boolean;
   mode: FibonacciRetracementMode;
diff --git a/src/core/Drawings/line/lineDrawing.ts b/src/core/Drawings/line/lineDrawing.ts
index 128255b28d9bd901e225f6d8c796e10de919980b..b21f8814cb7baf4267f114b63f04da81cd8b5e4e 100644
--- a/src/core/Drawings/line/lineDrawing.ts
+++ b/src/core/Drawings/line/lineDrawing.ts
@@ -5,6 +5,8 @@ import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase, TwoPointGeometry
 import { type ChartOptionsModel, Defaults, LineMarker, type SettingsTab } from '@src/types';
 import { formatDate, formatPrice } from '@src/utils';
 
+import { isAnchor } from '../utils';
+
 import { LineDrawingPaneView } from './paneView';
 import { createDefaultSettings, getLineDrawingSettingsTabs } from './settings';
 
@@ -24,6 +26,14 @@ interface LineDrawingParams extends BaseDrawingParams {
   defaultMarkers?: Partial<LineDrawingMarkers>;
 }
 
+export function isLineDrawingState(state: unknown): state is LineDrawingState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return 'startAnchor' in state && isAnchor(state.startAnchor) && 'endAnchor' in state && isAnchor(state.endAnchor);
+}
+
 interface LineDrawingState {
   hidden: boolean;
   mode: LineDrawingMode;
diff --git a/src/core/Drawings/ray/ray.ts b/src/core/Drawings/ray/ray.ts
index 46a152f0867d181894d1d1210a1698eb5ccf7261..a90c04a7ddd3d251334b15d6dfae716d30bc429f 100644
--- a/src/core/Drawings/ray/ray.ts
+++ b/src/core/Drawings/ray/ray.ts
@@ -1,6 +1,6 @@
 import { PrimitiveHoveredItem, UTCTimestamp } from 'lightweight-charts';
 
-import { getDistanceToSegment } from '@core/Drawings/utils';
+import { getDistanceToSegment, isAnchor } from '@core/Drawings/utils';
 
 import { BaseDrawingParams, ISeriesDrawing, LinearDrawingBase } from '@src/core/Drawings/base';
 import { type ChartOptionsModel, Defaults, type SettingsTab } from '@src/types';
@@ -23,6 +23,19 @@ type PriceLabelKind = 'start' | 'direction';
 
 type RayParams = BaseDrawingParams;
 
+export function isRayState(state: unknown): state is RayState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'startAnchor' in state &&
+    isAnchor(state.startAnchor) &&
+    'directionAnchor' in state &&
+    isAnchor(state.directionAnchor)
+  );
+}
+
 interface RayState {
   hidden: boolean;
   mode: RayMode;
diff --git a/src/core/Drawings/regressionTrend/regressionTrend.ts b/src/core/Drawings/regressionTrend/regressionTrend.ts
index 6ed24bfd367a9c32664674a6d64490e755e6eb9a..b9ad932850c9c26eeb097eb13ffb7b83a6b45774 100644
--- a/src/core/Drawings/regressionTrend/regressionTrend.ts
+++ b/src/core/Drawings/regressionTrend/regressionTrend.ts
@@ -25,6 +25,19 @@ type PriceLabelKind = 'start' | 'end';
 
 type RegressionTrendParams = BaseDrawingParams;
 
+export function isRegressionTrendState(state: unknown): state is RegressionTrendState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'startTime' in state &&
+    typeof state.startTime === 'number' &&
+    'endTime' in state &&
+    typeof state.endTime === 'number'
+  );
+}
+
 interface RegressionTrendState {
   hidden: boolean;
   mode: RegressionTrendMode;
diff --git a/src/core/Drawings/sliderPosition/sliderPosition.ts b/src/core/Drawings/sliderPosition/sliderPosition.ts
index c9abaa9ac3918de4726334d0c6b7d8ef2983537b..1fa5d8d7bbb73335e7c4a9e9dccbd55a5bf7b3c4 100644
--- a/src/core/Drawings/sliderPosition/sliderPosition.ts
+++ b/src/core/Drawings/sliderPosition/sliderPosition.ts
@@ -51,6 +51,25 @@ interface SliderPositionParams extends BaseDrawingParams {
   resetTriggers?: Observable<unknown>[];
 }
 
+export function isSliderPositionState(state: unknown): state is SliderPositionState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return (
+    'startTime' in state &&
+    typeof state.startTime === 'number' &&
+    'endTime' in state &&
+    typeof state.endTime === 'number' &&
+    'entryPrice' in state &&
+    typeof state.entryPrice === 'number' &&
+    'stopPrice' in state &&
+    typeof state.stopPrice === 'number' &&
+    'targetPrice' in state &&
+    typeof state.targetPrice === 'number'
+  );
+}
+
 interface SliderPositionState {
   hidden: boolean;
   mode: SliderMode;
diff --git a/src/core/Drawings/text/text.ts b/src/core/Drawings/text/text.ts
index 03c59c46ef8dfb2eb95b2ab6be10cfd3ddff0b65..9489acb4130a608630839b4dc7c9560896bd54ee 100644
--- a/src/core/Drawings/text/text.ts
+++ b/src/core/Drawings/text/text.ts
@@ -9,7 +9,7 @@ import {
   getYCoordinateFromPrice,
   isPointInBounds,
 } from '@core/Drawings/helpers';
-import { updateViews } from '@core/Drawings/utils';
+import { isAnchor, updateViews } from '@core/Drawings/utils';
 
 import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
 import { Defaults, SettingsTab } from '@src/types';
@@ -23,6 +23,14 @@ import type { ChartOptionsModel } from '@src/types';
 
 type TextMode = 'idle' | 'dragging' | 'ready';
 
+export function isTextState(state: unknown): state is TextState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return 'point' in state && isAnchor(state.point);
+}
+
 export interface TextState {
   hidden: boolean;
   mode: TextMode;
diff --git a/src/core/Drawings/traectory/traectory.ts b/src/core/Drawings/traectory/traectory.ts
index 8d2fb230572b8d8ba90ae0309c7ca6a990f4c88d..aad51bf06c25bc85543f7e8f7e287e24f00a8512 100644
--- a/src/core/Drawings/traectory/traectory.ts
+++ b/src/core/Drawings/traectory/traectory.ts
@@ -2,7 +2,7 @@ import { PrimitiveHoveredItem } from 'lightweight-charts';
 import { clamp } from 'lodash-es';
 
 import { clampPointToContainer, getContainerSize, isNearPoint } from '@core/Drawings/helpers';
-import { getDistanceToSegment } from '@core/Drawings/utils';
+import { getDistanceToSegment, isAnchor } from '@core/Drawings/utils';
 
 import { BaseDrawingParams, DrawingBase, ISeriesDrawing } from '@src/core/Drawings/base';
 
@@ -18,6 +18,14 @@ type TraectoryMode = 'idle' | 'drawing' | 'ready' | 'dragging-point' | 'dragging
 type TraectoryHandleId = `${number}`;
 type TraectoryParams = BaseDrawingParams;
 
+export function isTraectoryState(state: unknown): state is TraectoryState {
+  if (typeof state !== 'object' || state === null) {
+    return false;
+  }
+
+  return 'points' in state && Array.isArray(state.points) && state.points.every((point) => isAnchor(point));
+}
+
 export interface TraectoryState {
   hidden: boolean;
   mode: TraectoryMode;
diff --git a/src/core/Drawings/utils.ts b/src/core/Drawings/utils.ts
index a00fdf76fd8222319e2578fd891a5a0bf8baf12b..92cbcc6a3b8f89503446106dcd7173e2351a9b68 100644
--- a/src/core/Drawings/utils.ts
+++ b/src/core/Drawings/utils.ts
@@ -1,4 +1,4 @@
-import type { Point, UpdatableView } from './types';
+import type { Anchor, Point, UpdatableView } from './types';
 
 export function updateViews(views: readonly UpdatableView[]): void {
   for (const view of views) {
@@ -66,3 +66,14 @@ export function getDistanceToSegment(point: Point, startPoint: Point, endPoint:
 
   return Math.hypot(point.x - projectionX, point.y - projectionY);
 }
+
+export function isAnchor(value: unknown): value is Anchor {
+  return (
+    value !== null &&
+    typeof value === 'object' &&
+    'time' in value &&
+    typeof value.time === 'number' &&
+    'price' in value &&
+    typeof value.price === 'number'
+  );
+}
diff --git a/src/core/DrawingsManager.tsx b/src/core/DrawingsManager.tsx
index 5b1f486c6e87eaff0dd87fbceb90637769eb5ccb..87ed194714fbeb9886ef4e46b93e2bf6c934cff8 100644
--- a/src/core/DrawingsManager.tsx
+++ b/src/core/DrawingsManager.tsx
@@ -13,12 +13,13 @@ import { ModalRenderer } from '@src/core/ModalRenderer';
 import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
 import { ActiveDrawingTool, DOMObjectSnapshot } from '@src/types';
 
+import { COPPIED_DRWAING_ID } from './DrawingsManagerCollection';
 import { Pane } from './Pane';
 
 import type { DrawingInteraction } from '@src/core/Drawings/base';
 import type { SettingsValues } from '@src/types/settings';
 
-interface DrawingsManagerParams {
+export interface DrawingsManagerParams {
   eventManager: EventManager;
   mainSeries$: Observable<SeriesStrategies | null>;
   lwcChart: IChartApi;
@@ -69,7 +70,6 @@ export class DrawingsManager {
   private selectedDrawingSnapshot: DrawingSnapshotItem | null = null;
   private pane: Pane;
 
-  private copyPasteBuffer: DrawingSnapshotItem | null = null;
   private setActiveTool: (name: ActiveDrawingTool) => void;
   private getActiveTool: () => ActiveDrawingTool;
   private getIsEndlessMode: () => boolean;
@@ -120,36 +120,14 @@ export class DrawingsManager {
       }),
     );
 
+    // todo: перенести в panе
     window.addEventListener('pointerup', this.handlePointerUp);
     window.addEventListener('pointercancel', this.handlePointerUp);
     this.container.addEventListener('click', this.handleClick);
     this.container.addEventListener('pointerdown', this.handlePointerDown);
     this.container.addEventListener('dblclick', this.handleDoubleClick);
     this.container.addEventListener('contextmenu', this.handleContextMenu);
-    // todo: implement ctrl+v
-    // hotkeys.register({
-    //   keys: [Keys.control, Keys.v],
-    //   callback: () => {
-    //     const bufferWithAppliedPosition = {
-    //       ...this.copyPasteBuffer,
-    //       state: {
-    //         ...this.copyPasteBuffer?.state,
-    //         startAnchor: {
-    //           price: 73.36210252637723,
-    //           time: 1783679170
-    //         }
-    //       }
-    //     }
-    //
-    //     this.setSnapshot([
-    //       ...this.getSnapshot(),
-    //       bufferWithAppliedPosition
-    //     ])
-    //     // hotkeys.unregister({ // todo: unregister all else ctrl+c's
-    //     //   keys: [Keys.control, Keys.c]
-    //     // })
-    //   }
-    // })
+    this.container.addEventListener('contextmenu', this.handleContextMenu);
   }
 
   // TODO: handlePointerDown конфликтует с DrawingsManagerCollection.handlePaneClick
@@ -240,6 +218,7 @@ export class DrawingsManager {
   };
 
   private handleClick = (event: MouseEvent): void => {
+    // todo: move method to pane
     if (!this.isEventInPane(event)) {
       return;
     }
@@ -248,6 +227,8 @@ export class DrawingsManager {
 
     pendingDrawing?.getSeriesDrawing().click(event);
 
+    this.pane.setSelected();
+
     this.DOM.refreshEntities();
   };
 
@@ -561,11 +542,6 @@ export class DrawingsManager {
         drawingName: name,
         name: drawingLabelById()[name],
         onDelete: this.removeDrawing,
-        onCopy: () => {
-          if (createdDrawing) {
-            this.copyPasteBuffer = this.createDrawingSnapshot(createdDrawing);
-          }
-        },
         zIndex: entityZIndex,
         moveDown,
         moveUp,
@@ -634,13 +610,20 @@ export class DrawingsManager {
       drawings.push(
         this.createDrawing({
           name: item.drawingName,
-          options: {
-            id: item.id,
-            state: cloneDeep(item.state),
-            isLocked: item.isLocked,
-            zIndex: item.zIndex,
-            shouldUpdateDrawingsList: false,
-          },
+          options:
+            item.id === COPPIED_DRWAING_ID
+              ? {
+                  state: cloneDeep(item.state),
+                  isLocked: item.isLocked,
+                  shouldUpdateDrawingsList: false,
+                }
+              : {
+                  id: item.id,
+                  state: cloneDeep(item.state),
+                  isLocked: item.isLocked,
+                  zIndex: item.zIndex,
+                  shouldUpdateDrawingsList: false,
+                },
         }),
       );
 
@@ -698,6 +681,16 @@ export class DrawingsManager {
     this.removeDrawing(drawing.id);
   }
 
+  public getSelectedDrawingSnapshot(): DrawingSnapshotItem | null {
+    const drawing = this.selectedDrawing$.value;
+
+    if (!drawing) {
+      return null;
+    }
+
+    return this.createDrawingSnapshot(drawing);
+  }
+
   public toggleSelectedDrawingLock(): void {
     const drawing = this.selectedDrawing$.value;
 
@@ -768,7 +761,6 @@ export class DrawingsManager {
     this.drawings$.value.forEach((drawing) => drawing.destroy());
 
     this.selectedDrawingSnapshot = null;
-    this.copyPasteBuffer = null;
 
     this.subscriptions.unsubscribe();
     this.drawings$.complete();
diff --git a/src/core/DrawingsManagerCollection.ts b/src/core/DrawingsManagerCollection.ts
index 06359f4739f3b061fc75e939bb071ed0629aa35c..faacd33abbe9fd62ec3d54152f6e742e04a91bdf 100644
--- a/src/core/DrawingsManagerCollection.ts
+++ b/src/core/DrawingsManagerCollection.ts
@@ -1,21 +1,50 @@
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
 
 import { DrawingsNames } from '@src/constants';
 import { DRAWING_KEYBOARD_SHORTCUTS, findDrawingPointerShortcut } from '@src/core/Drawings/shortcuts';
 import { ActiveDrawingTool, SettingsValues } from '@src/types';
 
+import { ChartMouseEvents } from './ChartMouseEvents';
 import { Drawing } from './Drawings';
-import { DrawingsManager } from './DrawingsManager';
+import { isAxisLineState } from './Drawings/axisLine/axisLine';
+import { isFibonacciRetracementState } from './Drawings/fibonacciRetracement/fibonacciRetracement';
+import {
+  getPriceFromYCoordinate,
+  getTimeFromXCoordinate,
+  getXCoordinateFromTime,
+  getYCoordinateFromPrice,
+} from './Drawings/helpers';
+import { isLineDrawingState } from './Drawings/line/lineDrawing';
+import { isRayState } from './Drawings/ray/ray';
+import { isRegressionTrendState } from './Drawings/regressionTrend/regressionTrend';
+import { isSliderPositionState } from './Drawings/sliderPosition/sliderPosition';
+import { isTextState } from './Drawings/text/text';
+import { isTraectoryState } from './Drawings/traectory/traectory';
+import { Anchor, SeriesApi } from './Drawings/types';
+import { DrawingsManager, DrawingSnapshotItem } from './DrawingsManager';
 import { Hotkeys, Keys } from './Hotkeys';
 
 import type { Pane } from './Pane';
+import type { IChartApi, MouseEventParams, Point, Time } from 'lightweight-charts';
 
-import type { MouseEventParams } from 'lightweight-charts';
+export interface DrawingsManagerCollectionParams {
+  hotkeys: Hotkeys;
+  selectedPane: Observable<Pane | null>;
+  subscribeChartEvent: ChartMouseEvents['subscribe'];
+  lwcChart: IChartApi;
+}
+
+export const COPPIED_DRWAING_ID = 'coppied';
 
-// todo: нужно дописывать класс)
 export class DrawingsManagerCollection {
   private managersMap: Map<number, DrawingsManager> = new Map();
   private hotkeys: Hotkeys;
+  private lwcChart: IChartApi;
+  private selectedPane: Pane | null = null;
+  private copyPasteBuffer: DrawingSnapshotItem | null = null;
+  private cursorPos = new BehaviorSubject<null | Point>(null);
+  private hoveredPane = new BehaviorSubject<null | number>(null);
+  private copyPasteAnchor: Anchor | null = null;
 
   private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
   private endlessMode$ = new BehaviorSubject(false);
@@ -23,26 +52,26 @@ export class DrawingsManagerCollection {
   private isAwaitingDrawingStart = false;
   private unregisterHotkeys: (() => void)[] = [];
 
-  constructor({ hotkeys }: { hotkeys: Hotkeys }) {
+  private subscriptions = new Subscription();
+
+  constructor({ hotkeys, selectedPane, subscribeChartEvent, lwcChart }: DrawingsManagerCollectionParams) {
     this.hotkeys = hotkeys;
+    this.lwcChart = lwcChart;
 
-    for (const { keys, drawingName } of DRAWING_KEYBOARD_SHORTCUTS) {
-      this.unregisterHotkeys.push(
-        this.hotkeys.register({
-          keys,
-          callback: () => {
-            this.activateDrawingTool(drawingName);
-          },
-        }),
-      );
-    }
+    this.subscriptions.add(
+      subscribeChartEvent('crosshairMove', (param) => {
+        this.cursorPos.next(param.point ?? null);
+        this.hoveredPane.next(param.paneIndex ?? null);
+      }),
+    );
 
-    this.unregisterHotkeys.push(
-      this.hotkeys.register({
-        keys: [Keys.escape],
-        callback: this.activateCrosshair,
+    this.subscriptions.add(
+      selectedPane.subscribe((next) => {
+        this.selectedPane = next;
       }),
     );
+
+    this.hotkeysSetup();
   }
 
   public getIsEndlessMode(): boolean {
@@ -144,15 +173,310 @@ export class DrawingsManagerCollection {
       unregisterHotkey();
     }
 
+    this.subscriptions.unsubscribe();
+
     this.unregisterHotkeys = [];
 
     this.activeTool$.complete();
     this.endlessMode$.complete();
   }
 
+  public copy() {
+    // public only for tests and this is sad
+    const pane = this.selectedPane;
+    if (!pane || this.hoveredPane.value !== pane.paneIndex()) {
+      return;
+    }
+
+    const drawingManager = pane.getDrawingManager();
+
+    const yCoord = this.cursorPos.value?.y;
+    const xCoord = this.cursorPos.value?.x;
+
+    if (yCoord === undefined || xCoord === undefined) {
+      this.copyPasteBuffer = null;
+      return;
+    }
+
+    this.copyPasteBuffer = drawingManager.getSelectedDrawingSnapshot();
+    this.copyPasteAnchor = {
+      time: <Time>getTimeFromXCoordinate(this.lwcChart, xCoord),
+      price: <number>getPriceFromYCoordinate(pane.getLocalMainSeries() as SeriesApi, yCoord),
+    };
+  }
+
+  public paste() {
+    // public only for tests and this is sad
+    const pane = this.selectedPane;
+    if (!pane || this.hoveredPane.value !== pane.paneIndex()) {
+      return;
+    }
+
+    const drawingManager = pane.getDrawingManager();
+
+    const drawingToPaste = this.copyPasteBuffer;
+
+    if (!drawingToPaste) {
+      return;
+    }
+
+    const { state } = drawingToPaste;
+
+    const yCoord = this.cursorPos.value?.y;
+    const xCoord = this.cursorPos.value?.x;
+
+    if (yCoord === undefined || xCoord === undefined || !this.copyPasteAnchor) {
+      return;
+    }
+
+    const { time: prevTime, price: prevPrice } = this.copyPasteAnchor;
+
+    const onCopyYCoord = getYCoordinateFromPrice(pane.getLocalMainSeries() as SeriesApi, prevPrice);
+    const onCopyXCoord = getXCoordinateFromTime(this.lwcChart, prevTime, pane.getLocalMainSeries() as SeriesApi);
+
+    if (onCopyYCoord === null || onCopyXCoord === null) {
+      return;
+    }
+
+    const shift = {
+      priceDiff: yCoord - onCopyYCoord,
+      timeDiff: xCoord - onCopyXCoord,
+    };
+
+    const shiftedState = shiftDrawingStateCoordsTo(state, shift, this.lwcChart, pane.getLocalMainSeries() as SeriesApi);
+
+    const bufferWithAppliedPosition: DrawingSnapshotItem = {
+      ...drawingToPaste,
+      id: COPPIED_DRWAING_ID,
+      zIndex: drawingToPaste.zIndex,
+      state: shiftedState,
+    };
+
+    drawingManager.setSnapshot([...drawingManager.getSnapshot(), bufferWithAppliedPosition]);
+  }
+
   private cancelPendingDrawings(): void {
     for (const manager of this.managersMap.values()) {
       manager.cancelPendingDrawing();
     }
   }
+
+  private hotkeysSetup(): void {
+    for (const { keys, drawingName } of DRAWING_KEYBOARD_SHORTCUTS) {
+      this.unregisterHotkeys.push(
+        this.hotkeys.register({
+          keys,
+          callback: () => {
+            this.activateDrawingTool(drawingName);
+          },
+        }),
+      );
+    }
+
+    this.unregisterHotkeys.push(
+      this.hotkeys.register({
+        keys: [Keys.mod, Keys.c],
+        callback: () => this.copy(),
+      }),
+    );
+
+    this.unregisterHotkeys.push(
+      this.hotkeys.register({
+        keys: [Keys.mod, Keys.v],
+        callback: () => this.paste(),
+      }),
+    );
+
+    this.unregisterHotkeys.push(
+      this.hotkeys.register({
+        keys: [Keys.escape],
+        callback: this.activateCrosshair,
+      }),
+    );
+  }
+}
+
+export function shiftPointByLogicalCoords(
+  time: Time,
+  price: number,
+  shift: { timeDiff: number; priceDiff: number },
+  chart: IChartApi,
+  serie: SeriesApi,
+): { time: Time; price: number } {
+  const startTimeCoord = getXCoordinateFromTime(chart, time, serie);
+  const startPriceCoord = getYCoordinateFromPrice(serie, price);
+
+  if (startTimeCoord === null || startPriceCoord === null) {
+    return {
+      time,
+      price,
+    };
+  }
+
+  const resTime = getTimeFromXCoordinate(chart, startTimeCoord + shift.timeDiff);
+  const resPrice = getPriceFromYCoordinate(serie, startPriceCoord + shift.priceDiff);
+
+  return {
+    time: resTime ?? time,
+    price: resPrice ?? price,
+  };
+}
+
+export function shiftDrawingStateCoordsTo(
+  state: unknown,
+  shift: { timeDiff: number; priceDiff: number },
+  chart: IChartApi,
+  serie: SeriesApi,
+) {
+  if (isLineDrawingState(state)) {
+    // LineDrawing, ParallelChannelState, VolumeProfileState
+    return {
+      ...state,
+      startAnchor: shiftPointByLogicalCoords(
+        <Time>state.startAnchor?.time,
+        <number>state.startAnchor?.price,
+        shift,
+        chart,
+        serie,
+      ),
+      endAnchor: shiftPointByLogicalCoords(
+        <Time>state.endAnchor?.time,
+        <number>state.endAnchor?.price,
+        shift,
+        chart,
+        serie,
+      ),
+    };
+  }
+
+  if (isRayState(state)) {
+    return {
+      ...state,
+      startAnchor: shiftPointByLogicalCoords(
+        <Time>state.startAnchor?.time,
+        <number>state.startAnchor?.price,
+        shift,
+        chart,
+        serie,
+      ),
+      directionAnchor: shiftPointByLogicalCoords(
+        <Time>state.directionAnchor?.time,
+        <number>state.directionAnchor?.price,
+        shift,
+        chart,
+        serie,
+      ),
+    };
+  }
+
+  if (isSliderPositionState(state)) {
+    // SliderPositionState
+    const { time: startTime, price: entryPrice } = shiftPointByLogicalCoords(
+      <Time>state.startTime,
+      <number>state.entryPrice,
+      shift,
+      chart,
+      serie,
+    );
+    const { time: endTime, price: stopPrice } = shiftPointByLogicalCoords(
+      <Time>state.endTime,
+      <number>state.stopPrice,
+      shift,
+      chart,
+      serie,
+    );
+    const { price: targetPrice } = shiftPointByLogicalCoords(
+      <Time>state.endTime,
+      <number>state.targetPrice,
+      shift,
+      chart,
+      serie,
+    );
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+      entryPrice,
+      stopPrice,
+      targetPrice,
+    };
+  }
+  if (isFibonacciRetracementState(state)) {
+    // FibonacciRetracement, DiapsonState, RectangleState
+    const { time: startTime, price: startPrice } = shiftPointByLogicalCoords(
+      <Time>state.startTime,
+      <number>state.startPrice,
+      shift,
+      chart,
+      serie,
+    );
+    const { time: endTime, price: endPrice } = shiftPointByLogicalCoords(
+      <Time>state.endTime,
+      <number>state.endPrice,
+      shift,
+      chart,
+      serie,
+    );
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+      startPrice,
+      endPrice,
+    };
+  }
+  if (isRegressionTrendState(state)) {
+    // RegressionTrend
+    const { time: startTime } = shiftPointByLogicalCoords(<Time>state.startTime, 0, shift, chart, serie);
+    const { time: endTime } = shiftPointByLogicalCoords(<Time>state.endTime, 0, shift, chart, serie);
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+    };
+  }
+
+  if (isAxisLineState(state)) {
+    // AxisLine
+    if (state.time === null) {
+      const { price } = shiftPointByLogicalCoords(<Time>0, <number>state.price, shift, chart, serie);
+      return {
+        ...state,
+        price,
+      };
+    }
+
+    const { time } = shiftPointByLogicalCoords(state.time, 0, shift, chart, serie);
+
+    return {
+      ...state,
+      time,
+    };
+  }
+
+  if (isTextState(state)) {
+    // TextState
+    return {
+      ...state,
+      point: shiftPointByLogicalCoords(<Time>state.point?.time, <number>state.point?.price, shift, chart, serie),
+    };
+  }
+
+  if (isTraectoryState(state)) {
+    // TraectoryState
+
+    const points = state.points.map((point) => {
+      return shiftPointByLogicalCoords(point.time, point.price, shift, chart, serie);
+    });
+
+    return {
+      ...state,
+      points,
+    };
+  }
+
+  return state;
 }
diff --git a/src/core/Indicators/index.tsx b/src/core/Indicators/index.tsx
index aadeff35ba5b32315086118eebfe5152ffbbd80a..60c57bdf05b9f76eec104f60620fcf3727cb9c5f 100644
--- a/src/core/Indicators/index.tsx
+++ b/src/core/Indicators/index.tsx
@@ -1,4 +1,4 @@
-import { PriceScaleMode, SeriesType } from 'lightweight-charts';
+import { AutoscaleInfo, PriceScaleMode, SeriesType } from 'lightweight-charts';
 
 import { Indicator } from '@core/Indicator';
 import { emaIndicator } from '@core/Indicators/ema';
@@ -50,6 +50,18 @@ export const indicatorsMap = (): Partial<Record<IndicatorsIds, IndicatorConfig>>
           priceFormat: {
             type: 'volume',
           },
+          autoscaleInfoProvider: (baseImplementation: () => AutoscaleInfo | null) => {
+            const res = baseImplementation();
+            if (!res || !res.priceRange || (res.priceRange.maxValue === 0 && res.priceRange.minValue === 0)) {
+              return {
+                priceRange: {
+                  minValue: 0,
+                  maxValue: 1,
+                },
+              };
+            }
+            return res;
+          },
         },
         dataFormatter: (params) => volume(params as IndicatorDataFormatter<'Histogram'>),
       },
diff --git a/src/core/MoexChart.tsx b/src/core/MoexChart.tsx
index 0bc048234a3fdf439fad1a36b3cca422b880497a..063b2634a7c0d5cfdea904540c978b828f9924b9 100644
--- a/src/core/MoexChart.tsx
+++ b/src/core/MoexChart.tsx
@@ -10,6 +10,7 @@ import { Hotkeys, Keys } from '@core/Hotkeys';
 import { ModalRenderer } from '@core/ModalRenderer';
 
 import { FloatingDrawingToolbar } from '@src/components/FloatingToolbar';
+import { Loader } from '@src/components/Loader';
 import { SeasonalityControls } from '@src/components/SeasonalityControls';
 import { SettingsModal } from '@src/components/SettingsModal';
 import Toolbar from '@src/components/Toolbar';
@@ -137,6 +138,7 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
   private headerRenderer!: UIRenderer;
   private modalRenderer!: ModalRenderer;
   private toolbarRenderer: UIRenderer | undefined;
+  private loaderRenderer: UIRenderer | undefined;
   private controlBarRenderer?: UIRenderer;
   private footerRenderer?: UIRenderer;
   private drawingToolbarRenderer!: UIRenderer;
@@ -206,6 +208,7 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
       controlBarContainer,
       drawingToolbarContainer,
       footerContainer,
+      loaderContainer,
       toggleToolbar, // todo: move this function to toolbarModel
     } = ContainerManager.createContainers({
       parentContainer: this.rootContainer,
@@ -270,7 +273,9 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
     this.subscriptions.add(
       this.hotkeys.register({
         keys: [Keys.escape],
-        enabled: () => this.chartView.value === ChartViewType.Seasonality && modalContainer.hidden,
+        enabled: () => {
+          return !!(this.chartView.value === ChartViewType.Seasonality && modalContainer.hidden);
+        },
         callback: this.backToChart,
       }),
     );
@@ -303,6 +308,7 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
       drawingToolbarContainer,
       controlBarContainer,
       footerContainer,
+      loaderContainer,
     });
 
     this.timeScaleHoverController = new TimeScaleHoverController({
@@ -328,11 +334,22 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
     preset: ChartCollectionPreset,
     containers: Pick<
       ReturnType<typeof ContainerManager.createContainers>,
-      'headerContainer' | 'toolBarContainer' | 'drawingToolbarContainer' | 'controlBarContainer' | 'footerContainer'
+      | 'headerContainer'
+      | 'toolBarContainer'
+      | 'drawingToolbarContainer'
+      | 'controlBarContainer'
+      | 'footerContainer'
+      | 'loaderContainer'
     >,
   ): void {
-    const { headerContainer, toolBarContainer, drawingToolbarContainer, controlBarContainer, footerContainer } =
-      containers;
+    const {
+      headerContainer,
+      toolBarContainer,
+      drawingToolbarContainer,
+      controlBarContainer,
+      footerContainer,
+      loaderContainer,
+    } = containers;
 
     this.headerRenderer = new ReactRenderer(headerContainer);
     this.toolbarRenderer = new ReactRenderer(toolBarContainer);
@@ -345,6 +362,17 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
     if (preset.showBottomPanel) {
       this.footerRenderer = new ReactRenderer(footerContainer);
     }
+
+    this.loaderRenderer = new ReactRenderer(loaderContainer);
+    this.subscriptions.add(
+      this.dataSource.isLoading().subscribe((isLoading) => {
+        if (isLoading) {
+          this.loaderRenderer?.renderComponent(<Loader />);
+        } else {
+          this.loaderRenderer?.clear();
+        }
+      }),
+    );
   }
 
   public setSettings(settings: ChartSettingsSource): void {
diff --git a/src/core/Pane.tsx b/src/core/Pane.tsx
index 0484870b14f536ffdf4d930fc32eccb47c1a5545..df0333525801649e77acf2a3b51262d15fe39d37 100644
--- a/src/core/Pane.tsx
+++ b/src/core/Pane.tsx
@@ -58,6 +58,7 @@ export interface PaneParams {
   getActiveTool: () => ActiveDrawingTool;
   getIsEndlessMode: () => boolean;
   continueDrawing: (name: DrawingsNames) => void;
+  deselectAllPanes: () => void;
 }
 
 // todo: Pane, ему должна принадлежать mainSerie, а также IndicatorManager и drawingsManager, mouseEvents. Также перекинуть соответствующие/необходимые свойства из чарта, и из чарта удалить
@@ -94,6 +95,8 @@ export class Pane implements ISerializable<PaneSnapshot> {
   private readonly onPriceScaleStateChange: () => void;
   private readonly subscriptions = new Subscription();
   private paneContainerSyncFrameId: number | null = null;
+  private isSelected = new BehaviorSubject<boolean>(false);
+  private deselectAllPanes: () => void;
 
   constructor({
     lwcChart,
@@ -120,6 +123,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
     getActiveTool,
     getIsEndlessMode,
     continueDrawing,
+    deselectAllPanes,
   }: PaneParams) {
     this.onDelete = onDelete;
     this.onPriceScaleStateChange = onPriceScaleStateChange;
@@ -129,6 +133,7 @@ export class Pane implements ISerializable<PaneSnapshot> {
     this.subscribeChartEvent = subscribeChartEvent;
     this.isMain = isMainPane;
     this.id = id;
+    this.deselectAllPanes = deselectAllPanes;
 
     if (isMainPane) {
       this.lwcPane = this.lwcChart.panes()[MAIN_PANE_INDEX];
@@ -216,8 +221,25 @@ export class Pane implements ISerializable<PaneSnapshot> {
         this.legendContainer.style.display = hasRuler ? 'none' : '';
       }),
     );
+
+    this.subscriptions.add(
+      this.drawingsManager.entities().subscribe((drawings) => {
+        const hasRuler = drawings.some((drawing) => drawing.getDrawingName() === DrawingsNames.ruler);
+
+        this.legendContainer.style.display = hasRuler ? 'none' : '';
+      }),
+    );
   }
 
+  public setSelected = () => {
+    this.deselectAllPanes();
+    this.isSelected.next(true);
+  };
+
+  public deselected = () => {
+    this.isSelected.next(false);
+  };
+
   public getLocalMainSeries = () => {
     return this.localMainSeries.value;
   };
diff --git a/src/core/PaneManager.ts b/src/core/PaneManager.ts
index a2e59cbc7ffd88a421c57287c24a23be18694fa3..d27f85e29debf0ec752ed4ec7bfea9244fb317d9 100644
--- a/src/core/PaneManager.ts
+++ b/src/core/PaneManager.ts
@@ -1,4 +1,4 @@
-import { type Observable, Subscription } from 'rxjs';
+import { BehaviorSubject, type Observable, Subscription } from 'rxjs';
 
 import { DataSource } from '@core/DataSource';
 import { DrawingsManagerSnapshot } from '@core/DrawingsManager';
@@ -29,7 +29,8 @@ type CollectionDependencies =
   | 'setActiveTool'
   | 'getActiveTool'
   | 'getIsEndlessMode'
-  | 'continueDrawing';
+  | 'continueDrawing'
+  | 'deselectAllPanes';
 
 interface PaneManagerStartParams {
   compareEntities$: Observable<Indicator[]>;
@@ -48,6 +49,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
   private readonly lwcChart: IChartApi;
   private readonly panesMap = new Map<number, Pane>();
 
+  private selectedPane = new BehaviorSubject<Pane | null>(null);
   private mainPane: Pane;
   private nextPaneId: number;
   private priceAxisLabels: PriceAxisLabels | null = null;
@@ -57,6 +59,13 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
   private readonly subscriptions = new Subscription();
 
   constructor({ panesSnapshot, ...sharedPaneParams }: PaneManagerParams) {
+    this.drawingsManagerCollection = new DrawingsManagerCollection({
+      hotkeys: sharedPaneParams.hotkeys,
+      selectedPane: this.selectedPane.asObservable(),
+      subscribeChartEvent: sharedPaneParams.subscribeChartEvent,
+      lwcChart: sharedPaneParams.lwcChart,
+    });
+
     this.sharedPaneParams = {
       ...sharedPaneParams,
     };
@@ -65,10 +74,6 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
     const mainPaneSnapshot = panesSnapshot.find((paneSnapshot) => paneSnapshot.isMain);
     const mainPaneId = mainPaneSnapshot?.id ?? 0;
 
-    this.drawingsManagerCollection = new DrawingsManagerCollection({
-      hotkeys: sharedPaneParams.hotkeys,
-    });
-
     this.mainPane = new Pane({
       ...this.sharedPaneParams,
       ...this.getCollectionDependencies(),
@@ -229,19 +234,25 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
     this.panesMap.clear();
   }
 
+  private deselectAllPanes(): void {
+    this.panesMap.forEach((pane) => {
+      pane.deselected();
+    });
+  }
+
   private initClickListener(): void {
     const handler = (param: MouseEventParams) => {
       if (param.paneIndex === undefined) {
         return;
       }
 
-      const clickedPane = this.getPaneByIndex(param.paneIndex);
+      this.selectedPane.next(this.getPaneByIndex(param.paneIndex) ?? null);
 
-      if (!clickedPane) {
+      if (!this.selectedPane.value) {
         return;
       }
 
-      this.drawingsManagerCollection.handlePaneClick(clickedPane, param);
+      this.drawingsManagerCollection.handlePaneClick(this.selectedPane.value, param);
     };
 
     this.lwcChart.subscribeClick(handler);
@@ -261,6 +272,10 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
       return;
     }
 
+    if (this.selectedPane.value?.getId() === id) {
+      this.selectedPane.next(null);
+    }
+
     const paneIndex = pane.paneIndex();
 
     this.panesMap.delete(id);
@@ -291,6 +306,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
       getActiveTool: () => this.drawingsManagerCollection.getActiveToolValue(),
       getIsEndlessMode: () => this.drawingsManagerCollection.getIsEndlessMode(),
       continueDrawing: (name: DrawingsNames) => this.drawingsManagerCollection.activateDrawingTool(name),
+      deselectAllPanes: () => this.deselectAllPanes(),
     };
   }
 
diff --git a/src/core/SymbolSource.ts b/src/core/SymbolSource.ts
index e76eb1afb520b30df1f03450c39a68a946d448c2..2a536b9b3c7d30990298c1da6a2712f95fe3249f 100644
--- a/src/core/SymbolSource.ts
+++ b/src/core/SymbolSource.ts
@@ -87,6 +87,10 @@ export class SymbolSource {
     return this.isLoadingSubject.value;
   }
 
+  public isLoading = (): Observable<boolean> => {
+    return this.isLoadingSubject.asObservable();
+  };
+
   public getOldestTime(): number | null {
     return this.oldestCandle?.time ?? null;
   }
diff --git a/src/core/__tests__/DrawingsManagerCollection.test.ts b/src/core/__tests__/DrawingsManagerCollection.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2317c2119509b7ce5438cfebb728ff50adeaf292
--- /dev/null
+++ b/src/core/__tests__/DrawingsManagerCollection.test.ts
@@ -0,0 +1,1023 @@
+import { IChartApi, MouseEventParams, Point, Time } from 'lightweight-charts';
+import { BehaviorSubject, Subscription } from 'rxjs';
+
+import { DrawingsManager, DrawingsManagerParams } from '@core/DrawingsManager';
+import {
+  DrawingsManagerCollection,
+  shiftDrawingStateCoordsTo,
+  shiftPointByLogicalCoords,
+} from '@core/DrawingsManagerCollection';
+import { Pane } from '@core/Pane';
+
+import { DrawingsNames } from '@src/constants';
+
+import { SettingsValues } from '@src/types';
+
+import { ChartMouseEvents } from '../ChartMouseEvents';
+import { SeriesApi } from '../Drawings/types';
+import { Hotkeys, Keys } from '../Hotkeys';
+
+jest.mock('@core/DrawingsManager');
+
+// ============ Factory Functions ============
+
+interface MockSeries {
+  data: jest.Mock<unknown[], []>;
+  coordinateToPrice: jest.Mock<number | null, [number]>;
+  priceToCoordinate: jest.Mock<number | null, [number]>;
+}
+
+function createMockSeries(): MockSeries {
+  return {
+    data: jest.fn(() => []),
+    coordinateToPrice: jest.fn((coord: number) => coord),
+    priceToCoordinate: jest.fn((price: number) => price),
+  };
+}
+
+function createMockTimeScale() {
+  const timeToCoordinate = jest.fn((time: Time): number | null => time as unknown as number);
+  const coordinateToTime = jest.fn((coord: number): Time | null => coord as Time);
+
+  return { timeToCoordinate, coordinateToTime };
+}
+
+function createMockChart(): { chart: IChartApi; timeScale: ReturnType<typeof createMockTimeScale> } {
+  const timeScale = createMockTimeScale();
+  const chart = {
+    timeScale: jest.fn(() => timeScale),
+  } as unknown as IChartApi;
+
+  return { chart, timeScale };
+}
+
+function createMockPane(params?: { id?: number; paneIndexValue?: number; series?: MockSeries | SeriesApi }): Pane {
+  const { id = 0, paneIndexValue = 0, series } = params ?? {};
+  const mockSeries = (series ?? createMockSeries()) as unknown as SeriesApi;
+  const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+
+  return {
+    getId: jest.fn(() => id),
+    paneIndex: jest.fn(() => paneIndexValue),
+    getDrawingManager: jest.fn(() => drawingManager),
+    getLocalMainSeries: jest.fn(() => mockSeries),
+    getHTMLElement: jest.fn(() => document.createElement('div')),
+    setSelected: jest.fn(),
+    deselected: jest.fn(),
+    isMainPane: jest.fn(() => id === 0),
+    getMainSerie: jest.fn(() => new BehaviorSubject(series ?? null)),
+    schedulePaneContainerSync: jest.fn(),
+    refreshPriceScaleControls: jest.fn(),
+    resetPriceScalesAutoScale: jest.fn(),
+    getSnapshot: jest.fn(),
+    getDrawingsSnapshot: jest.fn(),
+    setDrawingsSnapshot: jest.fn(),
+    setLocalMainSeries: jest.fn(),
+    setIndicator: jest.fn(),
+    removeIndicator: jest.fn(),
+    getPriceScale: jest.fn(),
+    isReady: jest.fn(() => Promise.resolve()),
+    destroy: jest.fn(),
+  } as unknown as Pane;
+}
+
+function createMockDrawingsManager(overrides?: Partial<DrawingsManager>) {
+  const defaultMocks = {
+    cancelPendingDrawing: jest.fn(),
+    startDrawing: jest.fn(),
+    openSelectedDrawingSettings: jest.fn(),
+    deleteSelectedDrawing: jest.fn(),
+    updateSelectedDrawingSettings: jest.fn(),
+    toggleSelectedDrawingLock: jest.fn(),
+    getSelectedDrawingSnapshot: jest.fn(() => null),
+    getSnapshot: jest.fn(() => []),
+    setSnapshot: jest.fn(),
+    entities: jest.fn(() => new BehaviorSubject([])),
+    selectedDrawing: jest.fn(() => new BehaviorSubject(null)),
+    destroy: jest.fn(),
+  };
+
+  return {
+    ...defaultMocks,
+    ...overrides,
+  } as unknown as DrawingsManager;
+}
+
+// ============ Tests ============
+
+describe('DrawingsManagerCollection', () => {
+  const DrawingManagerMock = DrawingsManager as jest.MockedClass<typeof DrawingsManager>;
+
+  let collection: DrawingsManagerCollection;
+  let pane: Pane;
+  let mockHotkeys: Hotkeys;
+  let mockChart: IChartApi;
+  let selectedPaneSubject: BehaviorSubject<Pane | null>;
+  let subscribeChartEventMock: jest.Mock;
+  let mockSeries: MockSeries;
+
+  beforeEach(() => {
+    mockSeries = createMockSeries();
+    mockChart = createMockChart().chart;
+    selectedPaneSubject = new BehaviorSubject<Pane | null>(null);
+    subscribeChartEventMock = jest.fn(() => () => {});
+    mockHotkeys = new Hotkeys();
+    pane = createMockPane({ series: mockSeries });
+
+    DrawingManagerMock.mockImplementation(() => createMockDrawingsManager());
+
+    collection = new DrawingsManagerCollection({
+      hotkeys: mockHotkeys,
+      selectedPane: selectedPaneSubject.asObservable(),
+      subscribeChartEvent: subscribeChartEventMock as unknown as ChartMouseEvents['subscribe'],
+      lwcChart: mockChart,
+    });
+  });
+
+  afterEach(() => {
+    collection.destroy();
+    mockHotkeys.destroy();
+    selectedPaneSubject.complete();
+    jest.clearAllMocks();
+  });
+
+  describe('Initialization', () => {
+    it('должен инициализироваться с пустой managersMap', () => {
+      expect((collection as unknown as { managersMap: Map<number, DrawingsManager> }).managersMap.size).toBe(0);
+    });
+
+    it('должен устанавливать активный инструмент в crosshair по умолчанию', () => {
+      expect(collection.getActiveToolValue()).toBe('crosshair');
+    });
+
+    it('должен устанавливать isAwaitingDrawingStart в false', () => {
+      expect((collection as unknown as { isAwaitingDrawingStart: boolean }).isAwaitingDrawingStart).toBe(false);
+    });
+
+    it('должен подписываться на selectedPane Observable', () => {
+      const newPane = createMockPane({ id: 1, paneIndexValue: 1 });
+      selectedPaneSubject.next(newPane);
+
+      const internalSelectedPane = (collection as unknown as { selectedPane: Pane | null }).selectedPane;
+      expect(internalSelectedPane).toBe(newPane);
+    });
+
+    it('должен подписываться на crosshairMove событие чарта', () => {
+      expect(subscribeChartEventMock).toHaveBeenCalledWith('crosshairMove', expect.any(Function));
+    });
+
+    it('должен обновлять cursorPos и hoveredPane при crosshairMove', () => {
+      const crosshairHandler = (
+        subscribeChartEventMock.mock.calls[0] as [string, (param: MouseEventParams) => void]
+      )[1];
+      const mockEvent = {
+        point: { x: 100, y: 200 } as Point,
+        paneIndex: 0,
+      };
+
+      crosshairHandler(mockEvent as unknown as MouseEventParams);
+
+      const { cursorPos } = collection as unknown as { cursorPos: BehaviorSubject<Point | null> };
+      const { hoveredPane } = collection as unknown as { hoveredPane: BehaviorSubject<number | null> };
+
+      expect(cursorPos.value).toEqual({ x: 100, y: 200 });
+      expect(hoveredPane.value).toBe(0);
+    });
+  });
+
+  describe('Drawing Manager Collection Management', () => {
+    it('Менеджер должен добавляться в коллекцию', () => {
+      const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+      collection.addDrawingManager(drawingManager, 1);
+
+      expect((collection as unknown as { managersMap: Map<number, DrawingsManager> }).managersMap.size).toBe(1);
+    });
+
+    it('Менеджер должен удаляться из коллекции', () => {
+      const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+      collection.addDrawingManager(drawingManager, 1);
+      collection.removeDrawingManager(1);
+
+      expect((collection as unknown as { managersMap: Map<number, DrawingsManager> }).managersMap.size).toBe(0);
+    });
+  });
+
+  describe('Active Tool Management', () => {
+    it('activateDrawingTool должен устанавливать инструмент и флаг isAwaitingDrawingStart', () => {
+      collection.activateDrawingTool(DrawingsNames.trendLine);
+
+      expect(collection.getActiveToolValue()).toBe(DrawingsNames.trendLine);
+      expect((collection as unknown as { isAwaitingDrawingStart: boolean }).isAwaitingDrawingStart).toBe(true);
+    });
+
+    it('activateDrawingTool должен отменять pending drawing во всех менеджерах', () => {
+      const manager1 = createMockDrawingsManager();
+      const manager2 = createMockDrawingsManager();
+
+      collection.addDrawingManager(manager1 as DrawingsManager, 0);
+      collection.addDrawingManager(manager2 as DrawingsManager, 1);
+
+      collection.activateDrawingTool(DrawingsNames.trendLine);
+
+      expect(manager1.cancelPendingDrawing).toHaveBeenCalledTimes(1);
+      expect(manager2.cancelPendingDrawing).toHaveBeenCalledTimes(1);
+    });
+
+    it('activateCrosshair должен переключать инструмент на crosshair', () => {
+      collection.activateDrawingTool(DrawingsNames.trendLine);
+      collection.activateCrosshair();
+
+      expect(collection.getActiveToolValue()).toBe('crosshair');
+      expect((collection as unknown as { isAwaitingDrawingStart: boolean }).isAwaitingDrawingStart).toBe(false);
+    });
+
+    it('activateCrosshair должен отменять pending drawing', () => {
+      const manager = createMockDrawingsManager();
+      collection.addDrawingManager(manager as DrawingsManager, 0);
+
+      collection.activateCrosshair();
+
+      expect(manager.cancelPendingDrawing).toHaveBeenCalledTimes(1);
+    });
+
+    it('setActiveTool должен устанавливать инструмент', () => {
+      collection.setActiveTool(DrawingsNames.horizontalLine);
+      expect(collection.getActiveToolValue()).toBe(DrawingsNames.horizontalLine);
+    });
+
+    it('setActiveTool на crosshair должен сбрасывать isAwaitingDrawingStart', () => {
+      collection.activateDrawingTool(DrawingsNames.trendLine);
+      collection.setActiveTool('crosshair');
+
+      expect((collection as unknown as { isAwaitingDrawingStart: boolean }).isAwaitingDrawingStart).toBe(false);
+    });
+
+    it('setActiveTool на drawing не должен менять isAwaitingDrawingStart', () => {
+      collection.activateDrawingTool(DrawingsNames.trendLine);
+      (collection as unknown as { isAwaitingDrawingStart: boolean }).isAwaitingDrawingStart = false;
+
+      collection.setActiveTool(DrawingsNames.horizontalLine);
+
+      expect((collection as unknown as { isAwaitingDrawingStart: boolean }).isAwaitingDrawingStart).toBe(false);
+    });
+
+    it('getActiveTool должен возвращать Observable', () => {
+      const observable = collection.getActiveTool();
+      expect(observable).toBeDefined();
+      expect(typeof observable.subscribe).toBe('function');
+    });
+  });
+
+  describe('Endless Drawing Mode', () => {
+    it('setEndlessDrawingMode должен устанавливать режим в true', () => {
+      collection.setEndlessDrawingMode(true);
+      expect(collection.getIsEndlessMode()).toBe(true);
+    });
+
+    it('setEndlessDrawingMode должен устанавливать режим в false', () => {
+      collection.setEndlessDrawingMode(true);
+      collection.setEndlessDrawingMode(false);
+      expect(collection.getIsEndlessMode()).toBe(false);
+    });
+
+    it('setEndlessDrawingMode не должен изменять значение при одинаковом значении', () => {
+      const { endlessMode$ } = collection as unknown as { endlessMode$: BehaviorSubject<boolean> };
+      const nextSpy = jest.spyOn(endlessMode$, 'next');
+
+      collection.setEndlessDrawingMode(false);
+
+      expect(nextSpy).not.toHaveBeenCalled();
+    });
+
+    it('getIsEndlessMode должен возвращать текущее значение', () => {
+      expect(collection.getIsEndlessMode()).toBe(false);
+      collection.setEndlessDrawingMode(true);
+      expect(collection.getIsEndlessMode()).toBe(true);
+    });
+
+    it('isEndlessDrawingsMode должен возвращать Observable', () => {
+      const observable = collection.isEndlessDrawingsMode();
+      expect(observable).toBeDefined();
+      expect(typeof observable.subscribe).toBe('function');
+    });
+  });
+
+  describe('handlePaneClick', () => {
+    let mockDrawingManager: DrawingsManager;
+
+    beforeEach(() => {
+      mockDrawingManager = createMockDrawingsManager();
+      jest.spyOn(pane, 'getDrawingManager').mockReturnValue(mockDrawingManager);
+    });
+
+    it('должен запускать drawing при pointer shortcut когда инструмент crosshair', () => {
+      collection.activateCrosshair();
+      selectedPaneSubject.next(pane);
+
+      const mockEvent = {
+        sourceEvent: { shiftKey: true, altKey: false, ctrlKey: false, metaKey: false },
+      } as unknown as MouseEventParams;
+
+      collection.handlePaneClick(pane, mockEvent);
+
+      expect(mockDrawingManager.startDrawing).toHaveBeenCalledWith(DrawingsNames.ruler, mockEvent);
+    });
+
+    it('не должен запускать drawing без pointer shortcut когда инструмент crosshair', () => {
+      collection.activateCrosshair();
+      selectedPaneSubject.next(pane);
+
+      collection.handlePaneClick(pane, {} as MouseEventParams);
+
+      expect(mockDrawingManager.startDrawing).not.toHaveBeenCalled();
+    });
+
+    it('должен запускать drawing когда активен инструмент и isAwaitingDrawingStart=true', () => {
+      collection.activateDrawingTool(DrawingsNames.verticalLine);
+      selectedPaneSubject.next(pane);
+
+      const mockEvent = {} as MouseEventParams;
+      collection.handlePaneClick(pane, mockEvent);
+
+      expect(mockDrawingManager.startDrawing).toHaveBeenCalledWith(DrawingsNames.verticalLine, mockEvent);
+      expect((collection as unknown as { isAwaitingDrawingStart: boolean }).isAwaitingDrawingStart).toBe(false);
+    });
+
+    it('не должен запускать drawing когда isAwaitingDrawingStart=false', () => {
+      collection.activateDrawingTool(DrawingsNames.verticalLine);
+      (collection as unknown as { isAwaitingDrawingStart: boolean }).isAwaitingDrawingStart = false;
+      selectedPaneSubject.next(pane);
+
+      collection.handlePaneClick(pane, {} as MouseEventParams);
+
+      expect(mockDrawingManager.startDrawing).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('Selected Drawing Operations', () => {
+    let mockDrawingManager: DrawingsManager;
+
+    beforeEach(() => {
+      mockDrawingManager = createMockDrawingsManager();
+      collection.addDrawingManager(mockDrawingManager, 0);
+    });
+
+    it('Should open selected drawing settings', () => {
+      collection.openSelectedDrawingSettings();
+      expect(mockDrawingManager.openSelectedDrawingSettings).toHaveBeenCalledTimes(1);
+    });
+
+    it('Should delete selected drawing', () => {
+      collection.deleteSelectedDrawing();
+      expect(mockDrawingManager.deleteSelectedDrawing).toHaveBeenCalledTimes(1);
+    });
+
+    it('Should toggle selected drawing lock', () => {
+      collection.toggleSelectedDrawingLock();
+      expect(mockDrawingManager.toggleSelectedDrawingLock).toHaveBeenCalledTimes(1);
+    });
+
+    it('Should update selected drawing settings', () => {
+      const settings = { lineColor: '#ff0000' } as SettingsValues;
+      collection.updateSelectedDrawingSettings(settings);
+      expect(mockDrawingManager.updateSelectedDrawingSettings).toHaveBeenCalledWith(settings);
+    });
+
+    it('selectedDrawing должен возвращать Observable', () => {
+      const observable = collection.selectedDrawing();
+      expect(observable).toBeDefined();
+      expect(typeof observable.subscribe).toBe('function');
+    });
+
+    it('updateSelectedDrawingSettings должен обновлять настройки во всех менеджерах', () => {
+      const manager1 = createMockDrawingsManager();
+      const manager2 = createMockDrawingsManager();
+
+      collection.addDrawingManager(manager1 as DrawingsManager, 0);
+      collection.addDrawingManager(manager2 as DrawingsManager, 1);
+
+      const settings = { lineColor: '#ff0000' } as SettingsValues;
+      collection.updateSelectedDrawingSettings(settings);
+
+      expect(manager1.updateSelectedDrawingSettings).toHaveBeenCalledWith(settings);
+      expect(manager2.updateSelectedDrawingSettings).toHaveBeenCalledWith(settings);
+    });
+  });
+
+  describe('Copy/Paste', () => {
+    let mockPane: Pane;
+    let mockDrawingManager: DrawingsManager;
+
+    beforeEach(() => {
+      mockDrawingManager = createMockDrawingsManager();
+      mockPane = createMockPane({ id: 0, paneIndexValue: 0, series: mockSeries });
+
+      jest.spyOn(mockPane, 'getDrawingManager').mockReturnValue(mockDrawingManager);
+      jest.spyOn(mockPane, 'getLocalMainSeries').mockReturnValue(mockSeries as never);
+
+      selectedPaneSubject.next(mockPane);
+
+      const { cursorPos } = collection as unknown as { cursorPos: BehaviorSubject<Point | null> };
+      cursorPos.next({ x: 100, y: 200 } as unknown as Point);
+
+      const { hoveredPane } = collection as unknown as { hoveredPane: BehaviorSubject<number | null> };
+      hoveredPane.next(0);
+    });
+
+    describe('copy', () => {
+      it('должен сохранять snapshot и anchor при успешном копировании', () => {
+        const snapshot = { id: 'test', drawingName: DrawingsNames.trendLine, state: {} as never };
+        jest.spyOn(mockDrawingManager, 'getSelectedDrawingSnapshot').mockReturnValue(snapshot);
+
+        collection.copy();
+
+        expect(mockDrawingManager.getSelectedDrawingSnapshot).toHaveBeenCalled();
+        const buffer = (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer;
+        const anchor = (collection as unknown as { copyPasteAnchor: unknown }).copyPasteAnchor;
+
+        expect(buffer).toEqual(snapshot);
+        expect(anchor).toEqual({ time: 100, price: 200 });
+      });
+
+      it('должен устанавливать null buffer при отсутствии координат cursorPos', () => {
+        const { cursorPos } = collection as unknown as { cursorPos: BehaviorSubject<Point | null> };
+        cursorPos.next(null);
+
+        collection.copy();
+
+        const buffer = (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer;
+        expect(buffer).toBeNull();
+      });
+
+      it('не должен работать если selectedPane отсутствует', () => {
+        selectedPaneSubject.next(null);
+
+        collection.copy();
+
+        const buffer = (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer;
+        expect(buffer).toBeNull();
+      });
+
+      it('не должен работать если hoveredPane не совпадает с selectedPane', () => {
+        const { hoveredPane } = collection as unknown as { hoveredPane: BehaviorSubject<number | null> };
+        hoveredPane.next(1);
+
+        collection.copy();
+
+        const buffer = (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer;
+        expect(buffer).toBeNull();
+      });
+    });
+
+    describe('paste', () => {
+      it('должен вставлять drawing со смещением', () => {
+        const snapshot = {
+          id: 'test',
+          drawingName: DrawingsNames.trendLine,
+          state: {
+            startAnchor: { time: 50 as Time, price: 100 },
+            endAnchor: { time: 60 as Time, price: 110 },
+          } as never,
+        };
+        (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer = snapshot;
+        (collection as unknown as { copyPasteAnchor: unknown }).copyPasteAnchor = { time: 50 as Time, price: 100 };
+
+        const { cursorPos } = collection as unknown as { cursorPos: BehaviorSubject<Point | null> };
+        cursorPos.next({ x: 150, y: 250 } as unknown as Point);
+
+        collection.paste();
+
+        expect(mockDrawingManager.setSnapshot).toHaveBeenCalled();
+      });
+
+      it('не должен работать без buffer', () => {
+        collection.paste();
+        expect(mockDrawingManager.setSnapshot).not.toHaveBeenCalled();
+      });
+
+      it('не должен работать если selectedPane отсутствует', () => {
+        (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer = {
+          id: 'test',
+          drawingName: DrawingsNames.trendLine,
+          state: {} as never,
+        };
+
+        selectedPaneSubject.next(null);
+
+        collection.paste();
+        expect(mockDrawingManager.setSnapshot).not.toHaveBeenCalled();
+      });
+
+      it('не должен работать если hoveredPane не совпадает', () => {
+        (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer = {
+          id: 'test',
+          drawingName: DrawingsNames.trendLine,
+          state: {} as never,
+        };
+
+        const { hoveredPane } = collection as unknown as { hoveredPane: BehaviorSubject<number | null> };
+        hoveredPane.next(1);
+
+        collection.paste();
+        expect(mockDrawingManager.setSnapshot).not.toHaveBeenCalled();
+      });
+
+      it('не должен работать при отсутствии cursorPos координат', () => {
+        (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer = {
+          id: 'test',
+          drawingName: DrawingsNames.trendLine,
+          state: {} as never,
+        };
+
+        const { cursorPos } = collection as unknown as { cursorPos: BehaviorSubject<Point | null> };
+        cursorPos.next(null);
+
+        collection.paste();
+        expect(mockDrawingManager.setSnapshot).not.toHaveBeenCalled();
+      });
+
+      it('не должен работать при отсутствии copyPasteAnchor', () => {
+        (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer = {
+          id: 'test',
+          drawingName: DrawingsNames.trendLine,
+          state: {} as never,
+        };
+
+        const { cursorPos } = collection as unknown as { cursorPos: BehaviorSubject<Point | null> };
+        cursorPos.next({ x: 150, y: 250 } as unknown as Point);
+
+        collection.paste();
+        expect(mockDrawingManager.setSnapshot).not.toHaveBeenCalled();
+      });
+
+      it('не должен работать при null координате якоря', () => {
+        (collection as unknown as { copyPasteBuffer: unknown }).copyPasteBuffer = {
+          id: 'test',
+          drawingName: DrawingsNames.trendLine,
+          state: {
+            startAnchor: { time: 50 as Time, price: 100 },
+            endAnchor: { time: 60 as Time, price: 110 },
+          } as never,
+        };
+        (collection as unknown as { copyPasteAnchor: unknown }).copyPasteAnchor = { time: 50 as Time, price: 100 };
+
+        const { cursorPos } = collection as unknown as { cursorPos: BehaviorSubject<Point | null> };
+        cursorPos.next({ x: 150, y: 250 } as unknown as Point);
+
+        mockSeries.priceToCoordinate.mockReturnValue(null);
+
+        collection.paste();
+        expect(mockDrawingManager.setSnapshot).not.toHaveBeenCalled();
+      });
+    });
+  });
+
+  describe('Destroy', () => {
+    it('destroy должен отписываться от всех подписок', () => {
+      const { subscriptions } = collection as unknown as { subscriptions: Subscription };
+      const unsubscribeSpy = jest.spyOn(subscriptions, 'unsubscribe');
+
+      collection.destroy();
+
+      expect(unsubscribeSpy).toHaveBeenCalled();
+    });
+
+    it('destroy должен очищать unregisterHotkeys массив', () => {
+      collection.destroy();
+      const { unregisterHotkeys } = collection as unknown as { unregisterHotkeys: (() => void)[] };
+      expect(unregisterHotkeys).toEqual([]);
+    });
+
+    it('destroy должен завершать activeTool$', () => {
+      const { activeTool$ } = collection as unknown as { activeTool$: BehaviorSubject<unknown> };
+      const completeSpy = jest.spyOn(activeTool$, 'complete');
+
+      collection.destroy();
+
+      expect(completeSpy).toHaveBeenCalled();
+    });
+
+    it('destroy должен завершать endlessMode$', () => {
+      const { endlessMode$ } = collection as unknown as { endlessMode$: BehaviorSubject<boolean> };
+      const completeSpy = jest.spyOn(endlessMode$, 'complete');
+
+      collection.destroy();
+
+      expect(completeSpy).toHaveBeenCalled();
+    });
+  });
+
+  describe('Hotkeys Setup', () => {
+    let registerSpy: jest.SpyInstance;
+
+    beforeEach(() => {
+      registerSpy = jest.spyOn(mockHotkeys, 'register');
+
+      collection = new DrawingsManagerCollection({
+        hotkeys: mockHotkeys,
+        selectedPane: selectedPaneSubject.asObservable(),
+        subscribeChartEvent: subscribeChartEventMock as unknown as ChartMouseEvents['subscribe'],
+        lwcChart: mockChart,
+      });
+    });
+
+    it('должен регистрировать хоткеи для инструментов рисования', () => {
+      const drawingShortcuts = registerSpy.mock.calls.filter((call) => call[0]?.keys?.includes(Keys.alt));
+
+      expect(drawingShortcuts.length).toBeGreaterThan(0);
+      expect(drawingShortcuts).toEqual(
+        expect.arrayContaining([
+          [expect.objectContaining({ keys: [Keys.alt, Keys.t] })],
+          [expect.objectContaining({ keys: [Keys.alt, Keys.h] })],
+          [expect.objectContaining({ keys: [Keys.alt, Keys.v] })],
+          [expect.objectContaining({ keys: [Keys.alt, Keys.f] })],
+          [expect.objectContaining({ keys: [Keys.alt, Keys.shift, Keys.r] })],
+        ]),
+      );
+    });
+
+    it('должен регистрировать хоткей для copy (mod+c)', () => {
+      const copyRegistration = registerSpy.mock.calls.find(
+        (call) => call[0]?.keys?.includes(Keys.mod) && call[0]?.keys?.includes(Keys.c),
+      );
+
+      expect(copyRegistration).toBeDefined();
+      expect(typeof copyRegistration?.[0]?.callback).toBe('function');
+    });
+
+    it('должен регистрировать хоткей для paste (mod+v)', () => {
+      const pasteRegistration = registerSpy.mock.calls.find(
+        (call) => call[0]?.keys?.includes(Keys.mod) && call[0]?.keys?.includes(Keys.v),
+      );
+
+      expect(pasteRegistration).toBeDefined();
+      expect(typeof pasteRegistration?.[0]?.callback).toBe('function');
+    });
+
+    it('должен регистрировать хоткей для escape', () => {
+      const escapeRegistration = registerSpy.mock.calls.find((call) => call[0]?.keys?.includes(Keys.escape));
+
+      expect(escapeRegistration).toBeDefined();
+      expect(typeof escapeRegistration?.[0]?.callback).toBe('function');
+    });
+
+    it('callback хоткея инструмента рисования должен вызывать activateDrawingTool', () => {
+      const trendLineRegistration = registerSpy.mock.calls.find(
+        (call) => call[0]?.keys?.includes(Keys.alt) && call[0]?.keys?.includes(Keys.t),
+      );
+      const activateSpy = jest.spyOn(collection, 'activateDrawingTool');
+
+      trendLineRegistration?.[0]?.callback();
+
+      expect(activateSpy).toHaveBeenCalledWith(DrawingsNames.trendLine);
+    });
+
+    it('callback хоткея copy должен вызывать copy', () => {
+      const copyRegistration = registerSpy.mock.calls.find(
+        (call) => call[0]?.keys?.includes(Keys.mod) && call[0]?.keys?.includes(Keys.c),
+      );
+      const copySpy = jest.spyOn(collection, 'copy');
+
+      copyRegistration?.[0]?.callback();
+
+      expect(copySpy).toHaveBeenCalled();
+    });
+
+    it('callback хоткея paste должен вызывать paste', () => {
+      const pasteRegistration = registerSpy.mock.calls.find(
+        (call) => call[0]?.keys?.includes(Keys.mod) && call[0]?.keys?.includes(Keys.v),
+      );
+      const pasteSpy = jest.spyOn(collection, 'paste');
+
+      pasteRegistration?.[0]?.callback();
+
+      expect(pasteSpy).toHaveBeenCalled();
+    });
+
+    it('callback хоткея escape должен вызывать activateCrosshair', () => {
+      const escapeRegistration = registerSpy.mock.calls.find((call) => call[0]?.keys?.includes(Keys.escape));
+
+      collection.activateDrawingTool(DrawingsNames.trendLine);
+
+      escapeRegistration?.[0]?.callback();
+
+      expect(collection.getActiveToolValue()).toBe('crosshair');
+    });
+
+    it('destroy должен вызывать все функции регистрации хоткеев', () => {
+      const unregister = jest.fn();
+
+      registerSpy.mockReturnValue(unregister);
+
+      collection = new DrawingsManagerCollection({
+        hotkeys: mockHotkeys,
+        selectedPane: selectedPaneSubject.asObservable(),
+        subscribeChartEvent: subscribeChartEventMock as unknown as ChartMouseEvents['subscribe'],
+        lwcChart: mockChart,
+      });
+
+      collection.destroy();
+
+      expect(unregister).toHaveBeenCalled();
+      expect((collection as unknown as { unregisterHotkeys: (() => void)[] }).unregisterHotkeys).toEqual([]);
+    });
+  });
+});
+
+describe('shiftPointByLogicalCoords', () => {
+  let mockSeries: MockSeries;
+  let mockChart: IChartApi;
+  let mockTimeScale: ReturnType<typeof createMockTimeScale>;
+
+  beforeEach(() => {
+    mockTimeScale = createMockTimeScale();
+    mockSeries = createMockSeries();
+    mockChart = {
+      timeScale: jest.fn(() => mockTimeScale),
+    } as unknown as IChartApi;
+  });
+
+  it('должен смещать time и price на shift', () => {
+    const shift = { timeDiff: 100, priceDiff: 50 };
+
+    const result = shiftPointByLogicalCoords(50 as Time, 100, shift, mockChart, mockSeries as unknown as SeriesApi);
+
+    expect(result).toEqual({ time: 150, price: 150 });
+  });
+
+  it('должен возвращать исходные значения при null time координате', () => {
+    mockTimeScale.timeToCoordinate.mockReturnValue(null);
+
+    const shift = { timeDiff: 100, priceDiff: 50 };
+    const result = shiftPointByLogicalCoords(50 as Time, 100, shift, mockChart, mockSeries as unknown as SeriesApi);
+
+    expect(result).toEqual({ time: 50 as Time, price: 100 });
+  });
+
+  it('должен возвращать исходные значения при null price координате', () => {
+    mockSeries.priceToCoordinate.mockReturnValue(null);
+
+    const shift = { timeDiff: 100, priceDiff: 50 };
+    const result = shiftPointByLogicalCoords(50 as Time, 100, shift, mockChart, mockSeries as unknown as SeriesApi);
+
+    expect(result).toEqual({ time: 50 as Time, price: 100 });
+  });
+
+  it('должен возвращать исходное время при null результате coordinateToTime', () => {
+    mockTimeScale.timeToCoordinate.mockReturnValue(100 as number);
+    mockTimeScale.coordinateToTime.mockImplementation(() => null);
+
+    const shift = { timeDiff: 100, priceDiff: 50 };
+    const result = shiftPointByLogicalCoords(50 as Time, 100, shift, mockChart, mockSeries as unknown as SeriesApi);
+
+    expect(result).toEqual({ time: 50 as Time, price: 150 });
+  });
+
+  it('должен возвращать исходную цену при null результате coordinateToPrice', () => {
+    mockTimeScale.timeToCoordinate.mockReturnValue(100 as number);
+    mockSeries.coordinateToPrice.mockReturnValue(null);
+
+    const shift = { timeDiff: 100, priceDiff: 50 };
+    const result = shiftPointByLogicalCoords(50 as Time, 100, shift, mockChart, mockSeries as unknown as SeriesApi);
+
+    expect(result).toEqual({ time: 200, price: 100 });
+  });
+});
+
+describe('shiftDrawingStateCoordsTo', () => {
+  let mockSeries: MockSeries;
+  let mockChart: IChartApi;
+  let mockTimeScale: ReturnType<typeof createMockTimeScale>;
+
+  const series = () => mockSeries as unknown as SeriesApi;
+
+  beforeEach(() => {
+    mockSeries = createMockSeries();
+    mockTimeScale = createMockTimeScale();
+    mockChart = {
+      timeScale: jest.fn(() => mockTimeScale),
+    } as unknown as IChartApi;
+  });
+
+  const shift = { timeDiff: 100, priceDiff: 50 };
+
+  it('должен смещать LineDrawingState', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      startAnchor: { time: 10 as Time, price: 20 },
+      endAnchor: { time: 30 as Time, price: 40 },
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      startAnchor: { time: 110, price: 70 },
+      endAnchor: { time: 130, price: 90 },
+    });
+  });
+
+  it('должен смещать RayState', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      startAnchor: { time: 10 as Time, price: 20 },
+      directionAnchor: { time: 30 as Time, price: 40 },
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      startAnchor: { time: 110, price: 70 },
+      directionAnchor: { time: 130, price: 90 },
+    });
+  });
+
+  it('должен смещать SliderPositionState', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      startTime: 10 as Time,
+      endTime: 30 as Time,
+      entryPrice: 20,
+      stopPrice: 40,
+      targetPrice: 60,
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      startTime: 110,
+      endTime: 130,
+      entryPrice: 70,
+      stopPrice: 90,
+      targetPrice: 110,
+    });
+  });
+
+  it('должен смещать FibonacciRetracementState', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      startTime: 10 as Time,
+      endTime: 30 as Time,
+      startPrice: 20,
+      endPrice: 40,
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      startTime: 110,
+      endTime: 130,
+      startPrice: 70,
+      endPrice: 90,
+    });
+  });
+
+  it('должен смещать RegressionTrendState', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      startTime: 10 as Time,
+      endTime: 30 as Time,
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      startTime: 110,
+      endTime: 130,
+    });
+  });
+
+  it('должен смещать AxisLineState (вертикальная)', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      time: 10 as Time,
+      price: null,
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      time: 110,
+    });
+  });
+
+  it('должен смещать AxisLineState (горизонтальная)', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      time: null,
+      price: 20,
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      price: 70,
+    });
+  });
+
+  it('должен смещать TextState', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      point: { time: 10 as Time, price: 20 },
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      point: { time: 110, price: 70 },
+    });
+  });
+
+  it('должен смещать TraectoryState', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      points: [
+        { time: 10 as Time, price: 20 },
+        { time: 30 as Time, price: 40 },
+      ],
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      points: [
+        { time: 110, price: 70 },
+        { time: 130, price: 90 },
+      ],
+    });
+  });
+
+  it('должен возвращать состояние без изменений для неизвестного типа', () => {
+    const state = { unknown: 'type' };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toBe(state);
+  });
+
+  it('должен смещать ParallelChannelState (как LineDrawing)', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      startAnchor: { time: 5 as Time, price: 10 },
+      endAnchor: { time: 15 as Time, price: 20 },
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      startAnchor: { time: 105, price: 60 },
+      endAnchor: { time: 115, price: 70 },
+    });
+  });
+
+  it('должен смещать DiapsonState (как FibonacciRetracement)', () => {
+    const state = {
+      hidden: false,
+      mode: 'idle' as const,
+      startTime: 5 as Time,
+      endTime: 15 as Time,
+      startPrice: 10,
+      endPrice: 20,
+      settings: {},
+    };
+
+    const result = shiftDrawingStateCoordsTo(state, shift, mockChart, series());
+
+    expect(result).toEqual({
+      ...state,
+      startTime: 105,
+      endTime: 115,
+      startPrice: 60,
+      endPrice: 70,
+    });
+  });
+});
diff --git a/src/core/__tests__/MoexChart.test.ts b/src/core/__tests__/MoexChart.test.ts
index 8640b89478e79e0344efe25c77452192118d0518..6d95ebb904bd33b4c21bd856f7108f0a57fc5a2a 100644
--- a/src/core/__tests__/MoexChart.test.ts
+++ b/src/core/__tests__/MoexChart.test.ts
@@ -1,4 +1,4 @@
-import { BehaviorSubject } from 'rxjs';
+import { BehaviorSubject, of, Subscription } from 'rxjs';
 
 import { Keys } from '@core/Hotkeys';
 import { type ChartCollectionPreset, type IMoexChart, MoexChart } from '@core/MoexChart';
@@ -336,6 +336,7 @@ describe('MoexChart', () => {
 jest.mock('@core/ReactRenderer', () => ({
   ReactRenderer: jest.fn(() => ({
     renderComponent: jest.fn(),
+    clear: jest.fn(),
     destroy: jest.fn(),
   })),
 }));
@@ -346,6 +347,7 @@ interface RendererContainers {
   drawingToolbarContainer: HTMLElement;
   controlBarContainer: HTMLElement;
   footerContainer: HTMLElement;
+  loaderContainer: HTMLElement;
 }
 
 interface ChartSetupInternals {
@@ -356,6 +358,7 @@ interface ChartSetupInternals {
   drawingToolbarRenderer?: object;
   controlBarRenderer?: object;
   footerRenderer?: object;
+  loaderRenderer?: object;
 }
 
 function createSetupContext() {
@@ -370,16 +373,20 @@ function createSetupContext() {
     drawingToolbarContainer: document.createElement('div'),
     controlBarContainer: document.createElement('div'),
     footerContainer: document.createElement('div'),
+    loaderContainer: document.createElement('div'),
   };
 
   const chart = Object.assign(Object.create(MoexChart.prototype), {
     hotkeys: { register },
     eventManager: { getUndoRedo },
+    dataSource: { isLoading: jest.fn(() => of(false)) },
+    subscriptions: new Subscription(),
     headerRenderer: undefined,
     toolbarRenderer: undefined,
     drawingToolbarRenderer: undefined,
     controlBarRenderer: undefined,
     footerRenderer: undefined,
+    loaderRenderer: undefined,
   }) as ChartSetupInternals;
 
   return { chart, containers, register, getUndoRedo, undo, redo };
@@ -424,15 +431,17 @@ describe('MoexChart: настройка интерфейса', () => {
 
     chart.createRenderers({} as ChartCollectionPreset, containers);
 
-    expect(ReactRenderer).toHaveBeenCalledTimes(3);
+    expect(ReactRenderer).toHaveBeenCalledTimes(4);
     expect(ReactRenderer).toHaveBeenNthCalledWith(1, containers.headerContainer);
     expect(ReactRenderer).toHaveBeenNthCalledWith(2, containers.toolBarContainer);
     expect(ReactRenderer).toHaveBeenNthCalledWith(3, containers.drawingToolbarContainer);
+    expect(ReactRenderer).toHaveBeenNthCalledWith(4, containers.loaderContainer);
     expect(chart.headerRenderer).toBeDefined();
     expect(chart.toolbarRenderer).toBeDefined();
     expect(chart.drawingToolbarRenderer).toBeDefined();
     expect(chart.controlBarRenderer).toBeUndefined();
     expect(chart.footerRenderer).toBeUndefined();
+    expect(chart.loaderRenderer).toBeDefined();
   });
 
   it('должен создавать обе дополнительные панели при включённых флагах', () => {
@@ -446,11 +455,13 @@ describe('MoexChart: настройка интерфейса', () => {
       containers,
     );
 
-    expect(ReactRenderer).toHaveBeenCalledTimes(5);
+    expect(ReactRenderer).toHaveBeenCalledTimes(6);
     expect(ReactRenderer).toHaveBeenCalledWith(containers.controlBarContainer);
     expect(ReactRenderer).toHaveBeenCalledWith(containers.footerContainer);
+    expect(ReactRenderer).toHaveBeenCalledWith(containers.loaderContainer);
     expect(chart.controlBarRenderer).toBeDefined();
     expect(chart.footerRenderer).toBeDefined();
+    expect(chart.loaderRenderer).toBeDefined();
   });
 
   it.each([
@@ -461,7 +472,7 @@ describe('MoexChart: настройка интерфейса', () => {
 
     chart.createRenderers(preset as ChartCollectionPreset, containers);
 
-    expect(ReactRenderer).toHaveBeenCalledTimes(4);
+    expect(ReactRenderer).toHaveBeenCalledTimes(5);
     expect(Boolean(chart.controlBarRenderer)).toBe(preset.showControlBar);
     expect(Boolean(chart.footerRenderer)).toBe(preset.showBottomPanel);
   });
diff --git a/src/core/__tests__/Pane.test.ts b/src/core/__tests__/Pane.test.ts
index d425fb524776ca1157d81e679d9f23f978d1ffd2..61755d7ae33dd47bab015a96d81e5c09b15b7980 100644
--- a/src/core/__tests__/Pane.test.ts
+++ b/src/core/__tests__/Pane.test.ts
@@ -160,6 +160,7 @@ describe('Pane', () => {
       getActiveTool: jest.fn(),
       getIsEndlessMode: jest.fn(),
       continueDrawing: jest.fn(),
+      deselectAllPanes: jest.fn(),
     });
   }
 
diff --git a/src/core/__tests__/PaneManager.test.ts b/src/core/__tests__/PaneManager.test.ts
index 79adee94c2eee2d7958599a338f0ce1f4a08b7e9..0c965aa9bc4492e3f2d8e0f99a5b1402ebf89f04 100644
--- a/src/core/__tests__/PaneManager.test.ts
+++ b/src/core/__tests__/PaneManager.test.ts
@@ -7,6 +7,7 @@ import { PaneManager } from '@core/PaneManager';
 import { PriceAxisLabels } from '@core/PriceAxisLabels';
 
 import { IndicatorsIds } from '@src/constants';
+import { Direction } from '@src/types';
 import { PaneSnapshot } from '@src/types/snapshot';
 
 jest.mock('@core/Pane');
@@ -125,4 +126,69 @@ describe('PaneManager', () => {
 
     manager.destroy();
   });
+
+  it('Выставление шкал', () => {
+    const manager = createManager([
+      {
+        isMain: true,
+        id: 0,
+        indicators: [],
+        drawings: [],
+      },
+      {
+        isMain: false,
+        id: 1,
+        indicators: [],
+        drawings: [],
+      },
+      {
+        isMain: false,
+        id: 2,
+        indicators: [
+          {
+            indicatorType: IndicatorsIds.RSI,
+          },
+        ],
+        drawings: [],
+      },
+    ]);
+    manager.setPriceScaleSideVisible(Direction.Left, true);
+
+    expect(manager['leftPriceScaleVisible']).toBe(true);
+    expect(manager['rightPriceScaleVisible']).toBe(true);
+    expect(manager.getMainPane().getPriceScale(Direction.Left)).toBeDefined();
+
+    manager.destroy();
+  });
+
+  it('геттер пейнов', () => {
+    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.getPaneByIndex(0)).toMatchObject(manager.getMainPane());
+
+    manager.destroy();
+  });
 });