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


diff --git a/CHANGELOG.md b/CHANGELOG.md
index 945fc82c5ad51d762dfc1982f0a583419f6ec892..dc4ba10ed6cb94be88752e319cccbd4f8da4830d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,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 218997a9159538d440767ba04d405db92c47c8a7..3a4de390c06fd3bf39c58dc13e8a42d294f352bf 100644
--- a/src/core/ContainerManager.ts
+++ b/src/core/ContainerManager.ts
@@ -96,6 +96,15 @@ export class ContainerManager {
     chartAreaContainer.tabIndex = -1;
     chartAreaContainer.style.cursor = 'crosshair';
 
+    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');
     toolBarContainer.style.height = '100%';
     toolBarContainer.style.minHeight = '0';
@@ -123,7 +132,7 @@ export class ContainerManager {
     modalContainer.style.zIndex = ZIndex.Modal;
     modalContainer.hidden = true;
 
-    chartAreaContainer.append(controlBarContainer, drawingToolbarContainer);
+    chartAreaContainer.append(controlBarContainer, drawingToolbarContainer, loaderContainer);
     chartContainer.append(chartAreaContainer, modalContainer);
     parentContainer.append(headerContainer, chartContainer);
 
@@ -161,6 +170,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/__tests__/shortcuts.test.ts b/src/core/Drawings/__tests__/shortcuts.test.ts
index ec941c4692765df0357027330e3fba7ce5c4bce5..e65100ab26fbeafe524869a3ceec2df91f9c95bf 100644
--- a/src/core/Drawings/__tests__/shortcuts.test.ts
+++ b/src/core/Drawings/__tests__/shortcuts.test.ts
@@ -9,7 +9,7 @@ import { Keys } from '@src/core/Hotkeys';
 
 import type { MouseEventParams } from 'lightweight-charts';
 
-function createEvent({
+export function createEvent({
   shiftKey = false,
   altKey = false,
   ctrlKey = false,
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..7586556d4255844363b00cc0fab7311d083fe5c1 100644
--- a/src/core/DrawingsManagerCollection.ts
+++ b/src/core/DrawingsManagerCollection.ts
@@ -1,21 +1,45 @@
-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 } 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 +47,23 @@ export class DrawingsManagerCollection {
   private isAwaitingDrawingStart = false;
   private unregisterHotkeys: (() => void)[] = [];
 
-  constructor({ hotkeys }: { hotkeys: Hotkeys }) {
-    this.hotkeys = hotkeys;
+  private subscriptions = new Subscription();
 
-    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.escape],
-        callback: this.activateCrosshair,
+  constructor({ hotkeys, selectedPane, subscribeChartEvent, lwcChart }: DrawingsManagerCollectionParams) {
+    this.hotkeys = hotkeys;
+    this.lwcChart = lwcChart;
+    selectedPane.subscribe((next) => {
+      this.selectedPane = next;
+    });
+
+    this.subscriptions.add(
+      subscribeChartEvent('crosshairMove', (param) => {
+        this.cursorPos.next(param.point ?? null);
+        this.hoveredPane.next(param.paneIndex ?? null);
       }),
     );
+
+    this.hotkeysSetup();
   }
 
   public getIsEndlessMode(): boolean {
@@ -150,9 +171,237 @@ export class DrawingsManagerCollection {
     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 shift = {
+      priceDiff: <number>getPriceFromYCoordinate(pane.getLocalMainSeries() as SeriesApi, yCoord) - prevPrice,
+      timeDiff: <number>getTimeFromXCoordinate(this.lwcChart, xCoord) - <number>prevTime,
+    };
+
+    const shiftedState = shiftDrawingStateCoordsTo(state, shift);
+
+    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 shiftDrawingStateCoordsTo(state: unknown, shift: { timeDiff: number; priceDiff: number }) {
+  if (isLineDrawingState(state)) {
+    // LineDrawing, ParallelChannelState, VolumeProfileState
+    const startAnchor = {
+      time: ((state.startAnchor?.time as number) + shift.timeDiff) as Time,
+      price: (state.startAnchor?.price as number) + shift.priceDiff,
+    };
+    const endAnchor = {
+      time: ((state.endAnchor?.time as number) + shift.timeDiff) as Time,
+      price: (state.endAnchor?.price as number) + shift.priceDiff,
+    };
+    return {
+      ...state,
+      startAnchor,
+      endAnchor,
+    };
+  }
+  if (isRayState(state)) {
+    // Ray
+    const startAnchor = {
+      time: ((state.startAnchor?.time as number) + shift.timeDiff) as Time,
+      price: (state.startAnchor?.price as number) + shift.priceDiff,
+    };
+
+    const directionAnchor = {
+      time: ((state.directionAnchor?.time as number) + shift.timeDiff) as Time,
+      price: (state.directionAnchor?.price as number) + shift.priceDiff,
+    };
+
+    return {
+      ...state,
+      startAnchor,
+      directionAnchor,
+    };
+  }
+
+  if (isSliderPositionState(state)) {
+    // SliderPositionState
+    const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+    const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+    const entryPrice = (state.entryPrice as number) + shift.priceDiff;
+    const stopPrice = (state.stopPrice as number) + shift.priceDiff;
+    const targetPrice = (state.targetPrice as number) + shift.priceDiff;
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+      entryPrice,
+      stopPrice,
+      targetPrice,
+    };
+  }
+  if (isFibonacciRetracementState(state)) {
+    // FibonacciRetracement, DiapsonState, RectangleState
+    const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+    const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+    const startPrice = (state.startPrice as number) + shift.priceDiff;
+    const endPrice = (state.endPrice as number) + shift.priceDiff;
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+      startPrice,
+      endPrice,
+    };
+  }
+  if (isRegressionTrendState(state)) {
+    // RegressionTrend
+    const startTime = ((state.startTime as number) + shift.timeDiff) as Time;
+    const endTime = ((state.endTime as number) + shift.timeDiff) as Time;
+
+    return {
+      ...state,
+      startTime,
+      endTime,
+    };
+  }
+
+  if (isAxisLineState(state)) {
+    // AxisLine
+    if (state.time === null) {
+      return {
+        ...state,
+        price: (state.price as number) + shift.priceDiff,
+      };
+    }
+
+    return {
+      ...state,
+      time: ((state.time as number) + shift.timeDiff) as Time,
+    };
+  }
+
+  if (isTextState(state)) {
+    // TextState
+    const point = {
+      time: ((state.point?.time as number) + shift.timeDiff) as Time,
+      price: (state.point?.price as number) + shift.priceDiff,
+    };
+
+    return {
+      ...state,
+      point,
+    };
+  }
+
+  if (isTraectoryState(state)) {
+    // TraectoryState
+
+    const points = state.points.map((point) => {
+      const anchor = {
+        time: ((point.time as number) + shift.timeDiff) as Time,
+        price: (point.price as number) + shift.priceDiff,
+      };
+
+      return anchor;
+    });
+
+    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 7f6258f5392ba17db04f9f442a08313b5d4fb774..a5496358ab9bb037bdf03adf6bdb1463c6c987b2 100644
--- a/src/core/MoexChart.tsx
+++ b/src/core/MoexChart.tsx
@@ -8,6 +8,7 @@ import { DataSource, DataSourceParams } from '@core/DataSource';
 import { Hotkeys, Keys } from '@core/Hotkeys';
 import { ModalRenderer } from '@core/ModalRenderer';
 import { FloatingDrawingToolbar } from '@src/components/FloatingToolbar';
+import { Loader } from '@src/components/Loader';
 import { SettingsModal } from '@src/components/SettingsModal';
 
 import Toolbar from '@src/components/Toolbar';
@@ -110,6 +111,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;
@@ -173,6 +175,7 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
       controlBarContainer,
       drawingToolbarContainer,
       footerContainer,
+      loaderContainer,
       toggleToolbar, // todo: move this function to toolbarModel
     } = ContainerManager.createContainers({
       parentContainer: this.rootContainer,
@@ -243,8 +246,17 @@ export class MoexChart implements ISerializable<MoexChartSnapshot> {
 
     this.headerRenderer = new ReactRenderer(headerContainer);
     this.toolbarRenderer = new ReactRenderer(toolBarContainer);
+    this.loaderRenderer = new ReactRenderer(loaderContainer);
     this.drawingToolbarRenderer = new ReactRenderer(drawingToolbarContainer);
 
+    this.dataSource.isLoading().subscribe((isLoading) => {
+      if (isLoading) {
+        this.loaderRenderer?.renderComponent(<Loader />);
+      } else {
+        this.loaderRenderer?.clear();
+      }
+    });
+
     if (config.chartCollectionPreset.showControlBar) {
       this.controlBarRenderer = new ReactRenderer(controlBarContainer);
     }
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 7d478ebb1b6038a9745c3138ce4c550d79e1e886..86f5901bece0e4baf0811dd0ffb216dd3b2d09b5 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..f8700c4e00d12686c373eb75deeb776c33a81a99
--- /dev/null
+++ b/src/core/__tests__/DrawingsManagerCollection.test.ts
@@ -0,0 +1,420 @@
+import { IChartApi, MouseEventParams } from 'lightweight-charts';
+import { BehaviorSubject } from 'rxjs';
+
+import { DrawingsManager, DrawingsManagerParams } from '@core/DrawingsManager';
+import {
+  DrawingsManagerCollection,
+  DrawingsManagerCollectionParams,
+  shiftDrawingStateCoordsTo,
+} from '@core/DrawingsManagerCollection';
+import { Pane } from '@core/Pane';
+
+import { DrawingsNames } from '@src/constants';
+
+import { SettingValue } from '@src/types';
+
+import { Hotkeys } from '../Hotkeys';
+import { createEvent } from '../Drawings/__tests__/shortcuts.test';
+
+jest.mock('@core/DrawingsManager');
+
+describe('DrawingsManagerCollection', () => {
+  const DrawingManagerMock = DrawingsManager as jest.MockedClass<typeof DrawingsManager>;
+
+  let collection: DrawingsManagerCollection;
+  let pane: Pane;
+  const cancelPendingDrawing = jest.fn(() => {});
+  const startDrawing = jest.fn(() => {});
+  const openSelectedDrawingSettings = jest.fn(() => {});
+  const deleteSelectedDrawing = jest.fn(() => {});
+  const updateSelectedDrawingSettings = jest.fn(() => {});
+  const toggleSelectedDrawingLock = jest.fn(() => {});
+  const paneIndex = jest.fn(() => { 0 });
+  const selectedPane = new BehaviorSubject({
+    getId: jest.fn(() => 0),
+    paneIndex,
+  } as unknown as Pane | null);
+
+  beforeEach(() => {
+    DrawingManagerMock.mockImplementation(
+      (params) =>
+        ({
+          startDrawing,
+          cancelPendingDrawing,
+          openSelectedDrawingSettings,
+          deleteSelectedDrawing,
+          updateSelectedDrawingSettings,
+          toggleSelectedDrawingLock,
+          paneIndex,
+        }) as unknown as DrawingsManager,
+    );
+    pane = {
+      getDrawingManager: () => new DrawingsManager({} as DrawingsManagerParams),
+    } as Pane;
+
+    const hotkeys = {
+      register: () => {},
+    } as unknown as Hotkeys;
+
+    const subscribeChartEvent = jest.fn((event, handler) => {
+      return () => {};
+    }) as unknown as DrawingsManagerCollectionParams['subscribeChartEvent'];
+
+    const lwcChart = {} as IChartApi;
+
+    collection = new DrawingsManagerCollection({
+      hotkeys,
+      selectedPane,
+      subscribeChartEvent,
+      lwcChart,
+    });
+  });
+
+it('Менеджер должен добавляться в колекцию', () => {
+    expect(collection['managersMap'].size).toBe(0);
+
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+
+    expect(collection['managersMap'].size).toBe(1);
+  });
+
+  it('Менеджер должен удаляться из колекции', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+    collection.removeDrawingManager(1);
+
+    expect(collection['managersMap'].size).toBe(0);
+  });
+
+  it('Активация drawingTool должена вызывать методы  drawingManager', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+
+    collection.activateDrawingTool(DrawingsNames.trendLine);
+    expect(collection['activeTool$'].value).toBe(DrawingsNames.trendLine);
+    expect(cancelPendingDrawing).toHaveBeenCalledTimes(1);
+
+    collection.activateDrawingTool(DrawingsNames.traectory);
+    expect(collection['activeTool$'].value).toBe(DrawingsNames.traectory);
+    expect(cancelPendingDrawing).toHaveBeenCalledTimes(2);
+  });
+
+  it('Активация activateCrosshair должна переключать activeTool', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+
+    collection.activateCrosshair();
+    expect(cancelPendingDrawing).toHaveBeenCalledTimes(1);
+    expect(collection.getActiveToolValue()).toBe('crosshair');
+  });
+
+  it('Активация activateCrosshair должна переключать activeTool', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+
+    collection.activateCrosshair();
+    expect(cancelPendingDrawing).toHaveBeenCalledTimes(1);
+    expect(collection.getActiveToolValue()).toBe('crosshair');
+  });
+
+  it('Установка endlessDrawingMode', () => {
+    collection.setEndlessDrawingMode(true);
+    expect(collection.getIsEndlessMode()).toBe(true);
+    collection.setEndlessDrawingMode(true);
+    expect(collection.getIsEndlessMode()).toBe(true);
+  });
+
+  it('handlePaneClick должен начинать рисование в соответстующем DrawingManager', () => {
+    collection.activateDrawingTool(DrawingsNames.verticalLine);
+    const e = {} as MouseEventParams;
+    collection.handlePaneClick(pane, {} as MouseEventParams);
+    expect(startDrawing).toBeCalledWith(DrawingsNames.verticalLine, e);
+    expect(startDrawing).toBeCalledTimes(1);
+
+    collection.handlePaneClick(pane, {} as MouseEventParams);
+    expect(startDrawing).toBeCalledTimes(1);
+
+    collection.activateCrosshair()
+    collection.handlePaneClick(pane, createEvent({}));
+    expect(startDrawing).toBeCalledTimes(1);
+  });
+
+  it('Should open selected drawing settings', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+
+    collection.openSelectedDrawingSettings();
+    expect(openSelectedDrawingSettings).toHaveBeenCalledTimes(1);
+  });
+
+  it('Should delete selected drawings', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+
+    collection.deleteSelectedDrawing();
+    expect(deleteSelectedDrawing).toHaveBeenCalledTimes(1);
+  });
+
+  it('Should delete selected drawings', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+    collection.toggleSelectedDrawingLock();
+
+    expect(toggleSelectedDrawingLock).toHaveBeenCalledTimes(1);
+  });
+
+  it('Should update selected drawing settings', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+    const settings = {} as SettingValue;
+
+    collection.updateSelectedDrawingSettings(settings);
+    expect(updateSelectedDrawingSettings).toHaveBeenCalledWith(settings);
+  });
+
+  it('copy should work', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+
+    collection.copy();
+    expect(paneIndex).toHaveBeenCalledTimes(1);
+    expect(collection['copyPasteBuffer']).toBe(null);
+  })
+
+  it('paste should work', () => {
+    const drawingManager = new DrawingsManager({} as DrawingsManagerParams);
+    collection.addDrawingManager(drawingManager, 1);
+
+    collection.paste();
+    expect(collection['copyPasteBuffer']).toBe(null);
+  });
+
+  it('ray coords shift', () => {
+    const shift = {
+      timeDiff: 9432,
+      priceDiff: 156,
+    };
+    const rayState = {
+      hidden: false,
+      mode: 'idle',
+      startAnchor: {
+        time: 503200,
+        price: 12,
+      },
+      directionAnchor: {
+        time: 50000,
+        price: 23,
+      },
+      settings: {},
+    };
+
+    expect(shiftDrawingStateCoordsTo(rayState, shift)).toEqual({
+      ...rayState,
+      startAnchor: {
+        time: rayState.startAnchor.time + shift.timeDiff,
+        price: rayState.startAnchor.price + shift.priceDiff,
+      },
+      directionAnchor: {
+        time: rayState.directionAnchor.time + shift.timeDiff,
+        price: rayState.directionAnchor.price + shift.priceDiff,
+      },
+    });
+  });
+
+  it('LineDrawing coords shift', () => {
+    const shift = {
+      timeDiff: 9432,
+      priceDiff: 156,
+    };
+    const rayState = {
+      hidden: false,
+      mode: 'idle',
+      startAnchor: {
+        time: 5032400,
+        price: 142,
+      },
+      endAnchor: {
+        time: 500540,
+        price: 243,
+      },
+      settings: {},
+    };
+
+    expect(shiftDrawingStateCoordsTo(rayState, shift)).toEqual({
+      ...rayState,
+      startAnchor: {
+        time: rayState.startAnchor.time + shift.timeDiff,
+        price: rayState.startAnchor.price + shift.priceDiff,
+      },
+      endAnchor: {
+        time: rayState.endAnchor.time + shift.timeDiff,
+        price: rayState.endAnchor.price + shift.priceDiff,
+      },
+    });
+  });
+
+  it('SliderPosition coords shift', () => {
+    const shift = {
+      timeDiff: 9432,
+      priceDiff: 156,
+    };
+    const sliderPositionState = {
+      hidden: false,
+      mode: 'idle',
+      startTime: 3263,
+      endTime: 32532,
+      entryPrice: 50,
+      stopPrice: 60,
+      targetPrice: 70,
+      settings: {},
+    };
+
+    expect(shiftDrawingStateCoordsTo(sliderPositionState, shift)).toEqual({
+      ...sliderPositionState,
+      startTime: sliderPositionState.startTime + shift.timeDiff,
+      endTime: sliderPositionState.endTime + shift.timeDiff,
+      entryPrice: sliderPositionState.entryPrice + shift.priceDiff,
+      stopPrice: sliderPositionState.stopPrice + shift.priceDiff,
+      targetPrice: sliderPositionState.targetPrice + shift.priceDiff,
+    });
+  });
+
+  it('FibonacciRetracement coords shift', () => {
+    const shift = {
+      timeDiff: 9432,
+      priceDiff: 156,
+    };
+    const fibonacciRetracementState = {
+      hidden: false,
+      mode: 'idle',
+      startTime: 503200000,
+      endTime: 50000000,
+      startPrice: 12,
+      endPrice: 53,
+      settings: {},
+    };
+
+    expect(shiftDrawingStateCoordsTo(fibonacciRetracementState, shift)).toEqual({
+      ...fibonacciRetracementState,
+      startTime: fibonacciRetracementState.startTime + shift.timeDiff,
+      endTime: fibonacciRetracementState.endTime + shift.timeDiff,
+      startPrice: fibonacciRetracementState.startPrice + shift.priceDiff,
+      endPrice: fibonacciRetracementState.endPrice + shift.priceDiff,
+    });
+  });
+
+  it('RegressionTrend coords shift', () => {
+    const shift = {
+      timeDiff: 9432,
+      priceDiff: 156,
+    };
+    const regressionTrendState = {
+      hidden: false,
+      mode: 'idle',
+      startTime: 234,
+      endTime: 987,
+      settings: {},
+    };
+
+    expect(shiftDrawingStateCoordsTo(regressionTrendState, shift)).toEqual({
+      ...regressionTrendState,
+      startTime: regressionTrendState.startTime + shift.timeDiff,
+      endTime: regressionTrendState.endTime + shift.timeDiff,
+    });
+  });
+
+  it('AxisLine coords shift', () => {
+    const shift = {
+      timeDiff: 9432,
+      priceDiff: 156,
+    };
+
+    const vertAxisLineState = {
+      hidden: false,
+      mode: 'idle',
+      time: 234,
+      price: null,
+      settings: {},
+    };
+
+    const horzAxisLineState = {
+      hidden: false,
+      mode: 'idle',
+      time: null,
+      price: 457,
+      settings: {},
+    };
+
+    expect(shiftDrawingStateCoordsTo(vertAxisLineState, shift)).toEqual({
+      ...vertAxisLineState,
+      time: vertAxisLineState.time + shift.timeDiff,
+    });
+    expect(shiftDrawingStateCoordsTo(horzAxisLineState, shift)).toEqual({
+      ...horzAxisLineState,
+      price: horzAxisLineState.price + shift.priceDiff,
+    });
+  });
+
+  it('TextState coords shift', () => {
+    const shift = {
+      timeDiff: 9432,
+      priceDiff: 156,
+    };
+
+    const textState = {
+      hidden: false,
+      mode: 'idle',
+      point: {
+        time: 100500,
+        price: 366666,
+      },
+      settings: {},
+    };
+
+    expect(shiftDrawingStateCoordsTo(textState, shift)).toEqual({
+      ...textState,
+      point: {
+        time: textState.point.time + shift.timeDiff,
+        price: textState.point.price + shift.priceDiff,
+      },
+    });
+  });
+
+  it('TraectoryState coords shift', () => {
+    const shift = {
+      timeDiff: 9432,
+      priceDiff: 156,
+    };
+
+    const traectoryState = {
+      hidden: false,
+      mode: 'idle',
+      points: [
+        {
+          time: 100500,
+          price: 366666,
+        },
+        {
+          time: 100502,
+          price: 34279,
+        },
+        {
+          time: 100,
+          price: 3666,
+        },
+      ],
+      settings: {},
+    };
+
+    expect(shiftDrawingStateCoordsTo(traectoryState, shift)).toEqual({
+      ...traectoryState,
+      points: traectoryState.points.map((point) => {
+        return {
+          time: point.time + shift.timeDiff,
+          price: point.price + shift.priceDiff,
+        };
+      }),
+    });
+  });
+});
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..afe414bd1e4b777106b96f4860274f66216882d6 100644
--- a/src/core/__tests__/PaneManager.test.ts
+++ b/src/core/__tests__/PaneManager.test.ts
@@ -8,6 +8,7 @@ import { PriceAxisLabels } from '@core/PriceAxisLabels';
 
 import { IndicatorsIds } from '@src/constants';
 import { PaneSnapshot } from '@src/types/snapshot';
+import { Direction } from '@src/types';
 
 jest.mock('@core/Pane');
 jest.mock('@core/DrawingsManagerCollection');
@@ -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();
+  });
 });