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


diff --git a/src/store/selectors/__tests__/widgets.test.ts b/src/store/selectors/__tests__/widgets.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2dc4eb291956ae050b428a8a468fb4a22520b3e6
--- /dev/null
+++ b/src/store/selectors/__tests__/widgets.test.ts
@@ -0,0 +1,31 @@
+import { RootState } from '@store/setupStore';
+
+import { masterWidgetSelector, widgetByIdSelector } from '../widgets';
+
+const createState = (widgets: { id: number; master?: number | null }[]) => ({ widgets: { widgets } }) as unknown as RootState;
+
+describe('widgetByIdSelector', () => {
+  it('находит виджет по id', () => {
+    expect(widgetByIdSelector(2)(createState([{ id: 1 }, { id: 2 }]))).toEqual({ id: 2 });
+  });
+
+  it('возвращает undefined для неизвестного id', () => {
+    expect(widgetByIdSelector(3)(createState([{ id: 1 }]))).toBeUndefined();
+  });
+});
+
+describe('masterWidgetSelector', () => {
+  it('возвращает мастер-виджет слейва', () => {
+    const state = createState([{ id: 1 }, { id: 2, master: 1 }]);
+
+    expect(masterWidgetSelector(2)(state)).toEqual({ id: 1 });
+  });
+
+  it('возвращает undefined, если у виджета нет мастера', () => {
+    expect(masterWidgetSelector(2)(createState([{ id: 1 }, { id: 2, master: null }]))).toBeUndefined();
+  });
+
+  it('возвращает undefined, если виджет не найден', () => {
+    expect(masterWidgetSelector(3)(createState([{ id: 1 }]))).toBeUndefined();
+  });
+});
diff --git a/src/store/selectors/widgets.ts b/src/store/selectors/widgets.ts
index be376d2ec5aa37b622bb59c8778ec79b84eccadc..9b262a2daa0cb4a478a1a0c45bb7b52e04867cf9 100644
--- a/src/store/selectors/widgets.ts
+++ b/src/store/selectors/widgets.ts
@@ -18,6 +18,11 @@ export const createWidgetsSelectorByWorkspaceId = (workspaceId: number) =>
 export const widgetByIdSelector = (id: number) => (state: RootState) =>
   state.widgets.widgets.find((item) => item.id === id);
 
+export const masterWidgetSelector = (widgetId: number) => (state: RootState) => {
+  const masterId = widgetByIdSelector(widgetId)(state)?.master;
+  return masterId ? widgetByIdSelector(masterId)(state) : undefined;
+};
+
 export const widgetsByWorkspaceIdAndTypeSelector = (workspaceId: number, type: WidgetContentType) =>
   createSelector([widgetsSelector], (widgets) =>
     widgets.filter((item) => item.workspaceId === workspaceId && item.type === type),
diff --git a/src/widgets/Chart/__tests__/MoexChart.test.tsx b/src/widgets/Chart/__tests__/MoexChart.test.tsx
index fb55dd842eade476e1edba1aec2178ae9a1b80a0..8fca4235ecdb310c14f5e481ecee4491e799f058 100644
--- a/src/widgets/Chart/__tests__/MoexChart.test.tsx
+++ b/src/widgets/Chart/__tests__/MoexChart.test.tsx
@@ -101,6 +101,25 @@ describe('MoexChart', () => {
     });
   });
 
+  it('передаёт набор серий сравнения в хук графика', () => {
+    const compareSymbols = [{ symbolId: 'NTBVTFC:NTBVCT:B', symbol: '(180, 190]' }];
+
+    render(
+      <MoexChartComponent
+        symbolInfo={symbolInfo}
+        compareSymbols={compareSymbols}
+        widgetId={42}
+        addInstrumentFromModal={mockAddInstrumentFromModal}
+      />,
+    );
+
+    expect(mockUseMoexChart).toHaveBeenCalledWith({
+      symbolInfo,
+      indicativeData: undefined,
+      compareSymbols,
+    });
+  });
+
   it('should attach chart container ref', () => {
     // Arrange & Act
     renderComponent();
diff --git a/src/widgets/Chart/__tests__/getSnapshotCompareSymbolIds.test.ts b/src/widgets/Chart/__tests__/getSnapshotCompareSymbolIds.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3a3311f56c086afc0ad2cdf175c194a6e87dd4c3
--- /dev/null
+++ b/src/widgets/Chart/__tests__/getSnapshotCompareSymbolIds.test.ts
@@ -0,0 +1,25 @@
+import { getSnapshotCompareSymbolIds } from '../utils/getSnapshotCompareSymbolIds';
+
+import type { MoexChartSnapshotInput } from '@mx-charts/charts';
+
+type Indicator = MoexChartSnapshotInput['charts'][number]['panes'][number]['indicators'][number];
+
+const createSnapshot = (indicators: Indicator[]) => ({ charts: [{ panes: [{ indicators }] }] });
+
+const compareIndicator = (symbolId: string): Indicator =>
+  ({ symbolInfo: { symbolId }, seriesName: 'Line', scale: 'right' }) as unknown as Indicator;
+
+const volumeIndicator = { indicatorType: 'Volume' } as unknown as Indicator;
+
+describe('getSnapshotCompareSymbolIds', () => {
+  it('собирает symbolId серий сравнения из всех панелей', () => {
+    const snapshot = createSnapshot([volumeIndicator, compareIndicator('A'), compareIndicator('B')]);
+
+    expect(getSnapshotCompareSymbolIds(snapshot)).toEqual(['A', 'B']);
+  });
+
+  it('возвращает пустой список без серий сравнения', () => {
+    expect(getSnapshotCompareSymbolIds(createSnapshot([volumeIndicator]))).toEqual([]);
+    expect(getSnapshotCompareSymbolIds({ charts: [] })).toEqual([]);
+  });
+});
diff --git a/src/widgets/Chart/__tests__/isSameSymbolSet.test.ts b/src/widgets/Chart/__tests__/isSameSymbolSet.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d167398ac8f772172d5a53da2b5e4af0c4dfc617
--- /dev/null
+++ b/src/widgets/Chart/__tests__/isSameSymbolSet.test.ts
@@ -0,0 +1,19 @@
+import { isSameSymbolSet } from '../utils/isSameSymbolSet';
+
+describe('isSameSymbolSet', () => {
+  it('считает одинаковыми наборы с теми же символами в другом порядке', () => {
+    expect(isSameSymbolSet(['A', 'B'], ['B', 'A'])).toBe(true);
+  });
+
+  it('считает пустые наборы одинаковыми', () => {
+    expect(isSameSymbolSet([], [])).toBe(true);
+  });
+
+  it('различает наборы разной длины', () => {
+    expect(isSameSymbolSet(['A'], ['A', 'B'])).toBe(false);
+  });
+
+  it('различает наборы с разными символами', () => {
+    expect(isSameSymbolSet(['A', 'B'], ['A', 'C'])).toBe(false);
+  });
+});
diff --git a/src/widgets/Chart/__tests__/useChartComponentFacade.test.tsx b/src/widgets/Chart/__tests__/useChartComponentFacade.test.tsx
index fa0a746264403db63b5a68741bbba554510719b3..58414bd973be26a594ef9f8856333c75011a550a 100644
--- a/src/widgets/Chart/__tests__/useChartComponentFacade.test.tsx
+++ b/src/widgets/Chart/__tests__/useChartComponentFacade.test.tsx
@@ -158,6 +158,19 @@ describe('useChartComponentFacade', () => {
     });
   });
 
+  it('прокидывает набор серий сравнения из контекста привязки', () => {
+    const compareSymbols = [{ symbolId: 'NTBVTFC:NTBVCT:B', symbol: '(180, 190]' }];
+
+    mockUseChartPublicContext.mockReturnValue({
+      issKey: undefined,
+      compareSymbols,
+    });
+
+    const { result } = renderFacade();
+
+    expect(result.current.compareSymbols).toBe(compareSymbols);
+  });
+
   it('should update and save instrument selected from modal', () => {
     // Arrange
     const { result } = renderFacade();
diff --git a/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx b/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
index 2b9711f0c62a608eb2112bcac73e7e776d8a4b2e..ae2701ec8356002a2db6af84fb7034ae3353c055 100644
--- a/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
+++ b/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
@@ -87,6 +87,10 @@ describe('useChartPublicContext', () => {
 
     // Mock the useAppSelect to return our test data
     mockUseAppSelect.mockImplementation((selector) => {
+      if (selector.toString().includes('selectedChartSymbols')) {
+        return undefined;
+      }
+
       if (selector.toString().includes('publicContext')) {
         return mockPublicContext;
       }
@@ -257,6 +261,81 @@ describe('useChartPublicContext', () => {
     });
   });
 
+  it('при привязке берёт имена основного инструмента из набора мастера, а не из справочника', () => {
+    const selectedChartSymbols = [
+      { symbolId: 'MOEX:TEST1', symbol: '≤ 10000', symbolName: 'Ячмень, ≤ 10000' },
+      { symbolId: 'MOEX:TEST2', symbol: '(10000, 20000]', symbolName: 'Ячмень, (10000, 20000]' },
+    ];
+    const mockSetCurrInstrument = jest.fn();
+    mockUseAppSelect.mockImplementation((selector) => {
+      if (selector.toString().includes('selectedChartSymbols')) {
+        return selectedChartSymbols;
+      }
+      if (selector.toString().includes('publicContext')) {
+        return mockPublicContext;
+      }
+      return mockWidget;
+    });
+    renderHook(() =>
+      useChartPublicContext({
+        widgetId: 1,
+        setCurrInstrument: mockSetCurrInstrument,
+        getMasterInstrumentFromPublicContext: jest.fn().mockReturnValue('MOEX:TEST1'),
+      }),
+    );
+    expect(mockSetCurrInstrument).toHaveBeenCalledWith({
+      symbolId: 'MOEX:TEST1',
+      symbol: '≤ 10000',
+      symbolName: 'Ячмень, ≤ 10000',
+    });
+  });
+
+  it('отдаёт набор символов графика из properties мастера', () => {
+    const selectedChartSymbols = [{ symbolId: 'NTBVTFC:NTBVCT:B', symbol: '(180, 190]' }];
+
+    mockUseAppSelect.mockImplementation((selector) => {
+      if (selector.toString().includes('selectedChartSymbols')) {
+        return selectedChartSymbols;
+      }
+
+      if (selector.toString().includes('publicContext')) {
+        return mockPublicContext;
+      }
+
+      return mockWidget;
+    });
+
+    const { result } = renderHook(() =>
+      useChartPublicContext({
+        widgetId: 1,
+        setCurrInstrument: jest.fn(),
+        getMasterInstrumentFromPublicContext: jest.fn().mockReturnValue('MOEX:TEST1'),
+      }),
+    );
+
+    expect(result.current.compareSymbols).toEqual(selectedChartSymbols);
+  });
+
+  it('не отдаёт набор символов графика без мастера', () => {
+    mockUseAppSelect.mockImplementation((selector) => {
+      if (selector.toString().includes('publicContext')) {
+        return mockPublicContext;
+      }
+
+      return { ...mockWidget, master: undefined };
+    });
+
+    const { result } = renderHook(() =>
+      useChartPublicContext({
+        widgetId: 1,
+        setCurrInstrument: jest.fn(),
+        getMasterInstrumentFromPublicContext: jest.fn().mockReturnValue('MOEX:TEST1'),
+      }),
+    );
+
+    expect(result.current.compareSymbols).toBeUndefined();
+  });
+
   it('should not set instrument when contracts array is empty', () => {
     // Arrange
     const mockSetCurrInstrument = jest.fn();
diff --git a/src/widgets/Chart/__tests__/useMoexchart.test.tsx b/src/widgets/Chart/__tests__/useMoexchart.test.tsx
index 06dbdd6fa3e39067b05585bd6fb5065deaede1a7..8f9dd2dca562878331563db85014ba1164428d33 100644
--- a/src/widgets/Chart/__tests__/useMoexchart.test.tsx
+++ b/src/widgets/Chart/__tests__/useMoexchart.test.tsx
@@ -18,6 +18,9 @@ jest.mock('../components/MoexChart/dataSourceProvide', () => ({
 
 jest.mock('@mx-charts/charts', () => ({
   MoexChart: jest.fn(),
+  CompareMode: {
+    Absolute: 'ABS',
+  },
   Timeframes: {
     '10s': '10s',
     '1m': '1m',
@@ -126,6 +129,7 @@ type TimeframeChangeCallback = (timeframe: TimeframeValue) => void;
 interface TestComponentProps {
   symbolInfo?: SymbolInfoInput;
   indicativeData?: ChartIndicativeData;
+  compareSymbols?: SymbolInfoInput[];
 }
 
 const defaultSymbolInfo: SymbolInfoInput = {
@@ -153,10 +157,22 @@ describe('useMoexChart', () => {
   const mockGetDataSource = jest.fn();
   const mockRealtimeUnsubscribe = jest.fn();
 
+  const mockCompareClear = jest.fn();
+  const mockCompareSetSymbolMode = jest.fn();
+  const mockCompareGetAllEntities = jest.fn();
+
   const mockCompareManager = {
     id: 'compare-manager',
+    clear: mockCompareClear,
+    setSymbolMode: mockCompareSetSymbolMode,
+    getAllEntities: mockCompareGetAllEntities,
   };
 
+  const compareSymbols: SymbolInfoInput[] = [
+    { symbolId: 'NTBVTFC:NTBVCT:A', symbol: '≤ 180', symbolName: 'Пшеница, ≤ 180' },
+    { symbolId: 'NTBVTFC:NTBVCT:B', symbol: '(180, 190]', symbolName: 'Пшеница, (180, 190]' },
+  ];
+
   const mockSnapshot: MockChartSnapshot = {
     charts: [
       {
@@ -181,10 +197,12 @@ describe('useMoexChart', () => {
   const TestComponent = ({
     symbolInfo = defaultSymbolInfo,
     indicativeData,
+    compareSymbols: compareSymbolsProp,
   }: TestComponentProps): React.ReactElement => {
     hookResult = useMoexChart({
       symbolInfo,
       indicativeData,
+      compareSymbols: compareSymbolsProp,
     });
 
     return <div ref={hookResult.containerRef} />;
@@ -229,6 +247,7 @@ describe('useMoexChart', () => {
 
     mockGetSnapshot.mockReturnValue(mockSnapshot);
     mockGetCompareManager.mockReturnValue(mockCompareManager);
+    mockCompareGetAllEntities.mockReturnValue([]);
 
     mockMoexChart.mockImplementation((config: MockMoexChartConfig) => {
       lastMoexChartConfig = config;
@@ -875,6 +894,96 @@ describe('useMoexChart', () => {
     expect(mockState.moexChartState?.initialInterval).toBeUndefined();
   });
 
+  it('не трогает серии сравнения без набора от мастера', () => {
+    render(<TestComponent />);
+
+    expect(mockCompareClear).not.toHaveBeenCalled();
+    expect(mockCompareSetSymbolMode).not.toHaveBeenCalled();
+  });
+
+  it('применяет набор мастера после создания графика, пропуская основной инструмент', () => {
+    const symbolInfo: SymbolInfoInput = { symbolId: 'NTBVTFC:NTBVCT:A', symbol: '≤ 180' };
+
+    render(
+      <TestComponent
+        symbolInfo={symbolInfo}
+        compareSymbols={compareSymbols}
+      />,
+    );
+
+    expect(mockCompareClear).toHaveBeenCalledTimes(1);
+    expect(mockCompareSetSymbolMode).toHaveBeenCalledTimes(1);
+    expect(mockCompareSetSymbolMode).toHaveBeenCalledWith('Line', compareSymbols[1], 'ABS');
+  });
+
+  it('не пересобирает серии, если на графике уже тот же набор', () => {
+    mockCompareGetAllEntities.mockReturnValue([
+      { symbolId: 'NTBVTFC:NTBVCT:B' },
+      { symbolId: 'NTBVTFC:NTBVCT:A' },
+    ]);
+
+    render(<TestComponent compareSymbols={compareSymbols} />);
+
+    expect(mockCompareClear).not.toHaveBeenCalled();
+    expect(mockCompareSetSymbolMode).not.toHaveBeenCalled();
+  });
+
+  it('перестраивает серии при смене набора мастера', () => {
+    const { rerender } = render(<TestComponent compareSymbols={compareSymbols} />);
+
+    expect(mockCompareSetSymbolMode).toHaveBeenCalledTimes(2);
+
+    const nextSymbols: SymbolInfoInput[] = [{ symbolId: 'NTBVTFC:NTBVCT:C', symbol: '(190, 200]' }];
+
+    rerender(<TestComponent compareSymbols={nextSymbols} />);
+
+    expect(mockCompareClear).toHaveBeenCalledTimes(2);
+    expect(mockCompareSetSymbolMode).toHaveBeenCalledTimes(3);
+    expect(mockCompareSetSymbolMode).toHaveBeenLastCalledWith('Line', nextSymbols[0], 'ABS');
+  });
+
+  it('снимает серии мастера при отвязке графика', () => {
+    const { rerender } = render(<TestComponent compareSymbols={compareSymbols} />);
+
+    expect(mockCompareClear).toHaveBeenCalledTimes(1);
+
+    rerender(<TestComponent />);
+
+    expect(mockCompareClear).toHaveBeenCalledTimes(2);
+    expect(mockCompareSetSymbolMode).toHaveBeenCalledTimes(2);
+  });
+
+  it('не снимает серии без мастера, если сам их не добавлял', () => {
+    const { rerender } = render(<TestComponent />);
+
+    rerender(<TestComponent symbolInfo={{ symbolId: 'MOEX:GAZP' }} />);
+
+    expect(mockCompareClear).not.toHaveBeenCalled();
+  });
+
+  it('не пересобирает серии, восстановленные из сохранённого снапшота', () => {
+    mockMoexChartState = {
+      timeframe: Timeframes['1d'],
+      savedData: JSON.stringify({
+        charts: [
+          {
+            ...mockSnapshot.charts[0],
+            panes: [
+              {
+                indicators: compareSymbols.map((symbolInfo) => ({ symbolInfo, seriesName: 'Line', scale: 'right' })),
+              },
+            ],
+          },
+        ],
+      }),
+    };
+
+    render(<TestComponent compareSymbols={compareSymbols} />);
+
+    expect(mockCompareClear).not.toHaveBeenCalled();
+    expect(mockCompareSetSymbolMode).not.toHaveBeenCalled();
+  });
+
   it('should destroy chart on unmount', () => {
     const { unmount } = render(<TestComponent />);
 
diff --git a/src/widgets/Chart/components/MoexChart/MoexChart.tsx b/src/widgets/Chart/components/MoexChart/MoexChart.tsx
index 1d939d059543be4da49736fb48e064ef363fee26..6adf91254a6a8abf34beef480508e9d090d03034 100644
--- a/src/widgets/Chart/components/MoexChart/MoexChart.tsx
+++ b/src/widgets/Chart/components/MoexChart/MoexChart.tsx
@@ -5,7 +5,7 @@ import { SymbolSearchModal } from '@widgets/Chart/components/MoexChart/component
 import { CompareModal } from './components/CompareModal';
 import { useMoexChart } from './hooks';
 
-import type { ChartIndicativeData, SelectedInstrument } from '../../types';
+import type { ChartCompareSymbol, ChartIndicativeData, SelectedInstrument } from '../../types';
 
 import type { SymbolInfoInput } from '@mx-charts/charts';
 
@@ -14,11 +14,12 @@ import '@mx-charts/charts/dist/styles.css';
 interface TRProps {
   symbolInfo: SymbolInfoInput;
   indicativeData?: ChartIndicativeData;
+  compareSymbols?: ChartCompareSymbol[];
   widgetId: number;
   addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
 }
 
-function MoexChart({ symbolInfo, indicativeData, widgetId, addInstrumentFromModal }: TRProps) {
+function MoexChart({ symbolInfo, indicativeData, compareSymbols, widgetId, addInstrumentFromModal }: TRProps) {
   const {
     containerRef,
     isCompareOpen,
@@ -29,6 +30,7 @@ function MoexChart({ symbolInfo, indicativeData, widgetId, addInstrumentFromModa
   } = useMoexChart({
     symbolInfo,
     indicativeData,
+    compareSymbols,
   });
 
   const handleSymbolChange = (instrument: SelectedInstrument): void => {
diff --git a/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts b/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
index f39b336ed4d643f205362a03c79ef2d136ad2c54..fd04e4e30ab85884c5ea1b53249b17b6600ccd53 100644
--- a/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
+++ b/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
@@ -1,27 +1,33 @@
-import { Intervals, MoexChart, Timeframes } from '@mx-charts/charts';
+import { CompareMode, Intervals, MoexChart, Timeframes } from '@mx-charts/charts';
 import { useCallback, useEffect, useRef, useState } from 'react';
 
 import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';
 import { WidgetProperties } from '@widgets/Chart/properties/types';
 import { ChartIndicativeData } from '@widgets/Chart/types';
 
+import { getSnapshotCompareSymbolIds } from '../../../utils/getSnapshotCompareSymbolIds';
+import { isSameSymbolSet } from '../../../utils/isSameSymbolSet';
 import { MOEX_CHART_CONFIG } from '../constants';
 import { DataSourceProvider } from '../dataSourceProvide';
 
+import type { ChartCompareSymbol } from '../../../types';
 import type { __CompareManager__, ChartSeriesType, IMoexChart, SymbolInfoInput } from '@mx-charts/charts';
 
 interface TUseMoexChartProps {
   symbolInfo: SymbolInfoInput;
   indicativeData?: ChartIndicativeData;
+  compareSymbols?: ChartCompareSymbol[];
 }
 
+const COMPARE_SERIES_TYPE: ChartSeriesType = 'Line';
+
 interface SnapshotSettings {
   timeframe: Timeframes;
   interval?: Intervals;
   chartSeriesType?: ChartSeriesType;
 }
 
-export const useMoexChart = ({ symbolInfo, indicativeData }: TUseMoexChartProps) => {
+export const useMoexChart = ({ symbolInfo, indicativeData, compareSymbols }: TUseMoexChartProps) => {
   const moexChartState = useSelectProperties(
     (widgetProperties: Partial<WidgetProperties>) => widgetProperties.moexChartState,
   );
@@ -41,6 +47,8 @@ export const useMoexChart = ({ symbolInfo, indicativeData }: TUseMoexChartProps)
   const initialIntervalRef = useRef<Intervals | undefined>(moexChartState?.initialInterval);
   const initialChartSeriesTypeRef = useRef<ChartSeriesType | undefined>(moexChartState?.initialChartSeriesType);
   const updateTimeframeRef = useRef<((timeframe: Timeframes) => void) | null>(null);
+  const compareSymbolsRef = useRef<ChartCompareSymbol[] | undefined>(compareSymbols);
+  const compareAppliedRef = useRef(false);
 
   useEffect(() => {
     const currentSymbolInfo = currentSymbolInfoRef.current;
@@ -77,6 +85,47 @@ export const useMoexChart = ({ symbolInfo, indicativeData }: TUseMoexChartProps)
     });
   };
 
+  const applyCompareSymbols = useCallback((restoredSymbolIds?: string[]): void => {
+    const manager = compareManagerRef.current;
+    const symbols = compareSymbolsRef.current;
+
+    if (!manager) {
+      return;
+    }
+
+    if (!symbols) {
+      if (compareAppliedRef.current) {
+        manager.clear();
+        compareAppliedRef.current = false;
+      }
+
+      return;
+    }
+
+    const mainSymbolId = currentSymbolInfoRef.current.symbolId;
+    const nextSymbols = symbols.filter((symbol) => symbol.symbolId !== mainSymbolId);
+    const currentSymbolIds = restoredSymbolIds ?? manager.getAllEntities().map((entity) => entity.symbolId);
+
+    compareAppliedRef.current = true;
+
+    if (isSameSymbolSet(currentSymbolIds, nextSymbols.map((symbol) => symbol.symbolId))) {
+      return;
+    }
+
+    manager.clear();
+
+    Promise.all(
+      nextSymbols.map((symbol) => manager.setSymbolMode(COMPARE_SERIES_TYPE, symbol, CompareMode.Absolute)),
+    ).catch((error) => {
+      console.warn(error);
+    });
+  }, []);
+
+  useEffect(() => {
+    compareSymbolsRef.current = compareSymbols;
+    applyCompareSymbols();
+  }, [applyCompareSymbols, compareSymbols, symbolInfo.symbolId]);
+
   const saveSnapshot = useCallback((): void => {
     const snapshot = chartRef.current?.getSnapshot();
 
@@ -171,6 +220,7 @@ export const useMoexChart = ({ symbolInfo, indicativeData }: TUseMoexChartProps)
 
     chartRef.current = chart;
     compareManagerRef.current = chart.getCompareManager();
+    applyCompareSymbols(savedDataRef.current ? getSnapshotCompareSymbolIds(savedSnapshot) : undefined);
 
     if (initialInterval || initialChartSeriesType) {
       initialIntervalRef.current = undefined;
diff --git a/src/widgets/Chart/hooks/useChartComponentFacade.tsx b/src/widgets/Chart/hooks/useChartComponentFacade.tsx
index 4213d5932b3cf8d3316e48d68bad0e62e88a3a63..75ce158f00b6f4532425c83b2cd4b30f8036c12a 100644
--- a/src/widgets/Chart/hooks/useChartComponentFacade.tsx
+++ b/src/widgets/Chart/hooks/useChartComponentFacade.tsx
@@ -14,7 +14,7 @@ import { DEFAULT_SYMBOL } from '../const';
 import { useChartPublicContext } from './useChartPublicContext';
 
 import type { WidgetProperties } from '../properties/types';
-import type { ChartContainerProps, SelectedInstrument } from '../types';
+import type { ChartCompareSymbol, ChartContainerProps, SelectedInstrument } from '../types';
 
 import type { SymbolInfoInput } from '@mx-charts/charts';
 import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
@@ -29,6 +29,7 @@ interface UseChartComponentFacadeReturn {
   onDropInstrument: (symbolId: string, withUpdate?: boolean) => void;
   addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
   isOver: boolean;
+  compareSymbols: ChartCompareSymbol[] | undefined;
 }
 
 type SaveContentPropsOptions = Partial<WidgetProperties['chartState']> & {
@@ -218,7 +219,7 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
     widgetId
   })
 
-  useChartPublicContext({
+  const { compareSymbols } = useChartPublicContext({
     setCurrInstrument: onInstrumentChangeFromBind,
     widgetId,
     getMasterInstrumentFromPublicContext,
@@ -270,5 +271,6 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
     onDropInstrument,
     addInstrumentFromModal,
     isOver,
+    compareSymbols,
   };
 }
diff --git a/src/widgets/Chart/hooks/useChartPublicContext.ts b/src/widgets/Chart/hooks/useChartPublicContext.ts
index a50be700ce7b824360688d648ff5f1a792a9b15c..c1fa80252ab5df671306d9c3e217ac12e3e5c514 100644
--- a/src/widgets/Chart/hooks/useChartPublicContext.ts
+++ b/src/widgets/Chart/hooks/useChartPublicContext.ts
@@ -3,10 +3,12 @@ import { useEffect, useMemo } from 'react';
 import { useAppSelect } from '@hooks/useAppSelector';
 
 import { Contract, useContracts } from '@modules/contracts';
+import { masterWidgetSelector } from '@store/selectors/widgets';
 import { filterByUniqIssKey } from '@utils/filterByUniqIssKey';
 import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
 import { Widget } from 'types/Widgets';
 
+import type { ChartCompareSymbol } from '../types';
 import type { SymbolInfoInput } from '@mx-charts/charts';
 
 interface UseChartPublicContextArg {
@@ -17,6 +19,7 @@ interface UseChartPublicContextArg {
 
 interface UseChartPublicContextReturn {
   issKey: Contract['issKey'] | undefined;
+  compareSymbols: ChartCompareSymbol[] | undefined;
 }
 
 export function useChartPublicContext({
@@ -27,6 +30,13 @@ export function useChartPublicContext({
   const publicContext = useAppSelect((state) => state.publicContext.publicContext);
   const widget = useAppSelect((state) => state.widgets.widgets.find(({ id }) => id === widgetId));
 
+  const compareSymbols = useAppSelect(
+    (state) =>
+      masterWidgetSelector(widgetId)(state)?.widgetContentProps?.selectedChartSymbols as
+        | ChartCompareSymbol[]
+        | undefined,
+  );
+
   const { contracts } = useContracts();
 
   const instruments = useMemo(() => (contracts ? filterByUniqIssKey(contracts, ['issKey']) : []), [contracts]);
@@ -53,15 +63,18 @@ export function useChartPublicContext({
       return;
     }
 
+    const masterSymbol = compareSymbols?.find((symbol) => symbol.symbolId === instrument.issKey);
+
     setCurrInstrument({
       symbolId: instrument.issKey,
-      symbol: instrument.symbol ?? undefined,
-      symbolName: instrument.displayName ?? undefined,
+      symbol: masterSymbol?.symbol ?? instrument.symbol ?? undefined,
+      symbolName: masterSymbol?.symbolName ?? instrument.displayName ?? undefined,
     });
     // eslint-disable-next-line react-hooks/exhaustive-deps -- Посмотреть этот момент.
-  }, [publicContext, instruments, widget?.externalProperties, setCurrInstrument]);
+  }, [publicContext, instruments, widget?.externalProperties, compareSymbols, setCurrInstrument]);
 
   return {
     issKey,
+    compareSymbols: widget?.master ? compareSymbols : undefined,
   };
 }
diff --git a/src/widgets/Chart/index.tsx b/src/widgets/Chart/index.tsx
index f015ae64ac84d6241fc033f3ce692336eef2e73b..0f959170002538736a8f9d9b831c2c0036864480 100644
--- a/src/widgets/Chart/index.tsx
+++ b/src/widgets/Chart/index.tsx
@@ -28,6 +28,7 @@ export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
     onDropInstrument,
     addInstrumentFromModal,
     isOver,
+    compareSymbols,
   } = useChartComponentFacade(props);
 
   /* на дроп обновляем название бумаги в сторе
@@ -86,6 +87,7 @@ export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
           <MoexChart
             symbolInfo={symbolInfo}
             indicativeData={indicativeData}
+            compareSymbols={compareSymbols}
             widgetId={props.widgetId}
             addInstrumentFromModal={addInstrumentFromModal}
           />
diff --git a/src/widgets/Chart/types.ts b/src/widgets/Chart/types.ts
index 2a4a5aae338ec7f621e1ed9d9f14435fa8fdc1f8..b5b9f4a66f83230c884ddc1004228a659b792ad1 100644
--- a/src/widgets/Chart/types.ts
+++ b/src/widgets/Chart/types.ts
@@ -19,4 +19,10 @@ export interface SelectedInstrument {
   symbol: Contract['symbol'];
 }
 
+export interface ChartCompareSymbol {
+  symbolId: string;
+  symbol?: string;
+  symbolName?: string;
+}
+
 export type ChartContainerProps = WidgetContentBasicProps<WidgetProperties>;
diff --git a/src/widgets/Chart/utils/getSnapshotCompareSymbolIds.ts b/src/widgets/Chart/utils/getSnapshotCompareSymbolIds.ts
new file mode 100644
index 0000000000000000000000000000000000000000..843fd2cb6b2bca8f40a65e6a598ddd1acded8231
--- /dev/null
+++ b/src/widgets/Chart/utils/getSnapshotCompareSymbolIds.ts
@@ -0,0 +1,14 @@
+import type { MoexChartSnapshotInput } from '@mx-charts/charts';
+
+type SnapshotIndicators = MoexChartSnapshotInput['charts'][number]['panes'][number]['indicators'];
+
+type SnapshotWithIndicators = {
+  charts: { panes: { indicators: SnapshotIndicators }[] }[];
+};
+
+export const getSnapshotCompareSymbolIds = (snapshot: SnapshotWithIndicators): string[] =>
+  snapshot.charts.flatMap((chart) =>
+    chart.panes.flatMap((pane) =>
+      pane.indicators.flatMap((indicator) => ('symbolInfo' in indicator ? [indicator.symbolInfo.symbolId] : [])),
+    ),
+  );
diff --git a/src/widgets/Chart/utils/isSameSymbolSet.ts b/src/widgets/Chart/utils/isSameSymbolSet.ts
new file mode 100644
index 0000000000000000000000000000000000000000..371baae985aa57b0f81751f18d03ae9302ad0f9f
--- /dev/null
+++ b/src/widgets/Chart/utils/isSameSymbolSet.ts
@@ -0,0 +1,4 @@
+const toSortedKey = (symbolIds: string[]) => [...symbolIds].sort().join('\n');
+
+export const isSameSymbolSet = (current: string[], next: string[]): boolean =>
+  current.length === next.length && toSortedKey(current) === toSortedKey(next);
diff --git a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/ContractsTable.tsx b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/ContractsTable.tsx
index f030ca1f2c677d2074e5ab4e0602ca371ff6d891..018b1af9a1ea52300972a457d97d951debfb8508 100644
--- a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/ContractsTable.tsx
+++ b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/ContractsTable.tsx
@@ -18,6 +18,7 @@ import { useContracts } from './hooks/useContracts';
 import { prepareContractsRows } from './utils/prepareContractsRows';
 
 import type { ContractsTableDataItem } from './types';
+import type { WidgetProperties } from '../../properties/types';
 import type { NTB_CONTRACTS_SOURCE_GROUP } from '@modules/ntb/types';
 
 type ContractsTableProps = {
@@ -29,6 +30,8 @@ type ContractsTableProps = {
 
 const getRowKey = (record: ContractsTableDataItem) => `${record.productName}:${record.range}`;
 
+const getChartSymbols = (record: ContractsTableDataItem) => record.chartSymbols;
+
 export const ContractsTable: FC<ContractsTableProps> = ({
   sourceGroup,
   hidden,
@@ -43,7 +46,7 @@ export const ContractsTable: FC<ContractsTableProps> = ({
   const columns = useMemo(() => buildColumns(contracts), [contracts]);
   const dataSource = useMemo(() => prepareContractsRows(contracts), [contracts]);
 
-  const { handleRowClick } = useHandleRowClick();
+  const { handleRowClick } = useHandleRowClick<ContractsTableDataItem, WidgetProperties>({ getChartSymbols });
 
   const { onContextMenu, menuProps, items } = useContextMenu({ sourceGroup, onShowHistory });
 
diff --git a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/hooks/__tests__/useContextMenu.test.ts b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/hooks/__tests__/useContextMenu.test.ts
index e87b9e097d655753961fa9532597ffb8a8cdac61..ff6433383f69925625226f0d39a5269d6f8fcef0 100644
--- a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/hooks/__tests__/useContextMenu.test.ts
+++ b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/hooks/__tests__/useContextMenu.test.ts
@@ -1,25 +1,29 @@
 import { act, renderHook } from '@testing-library/react';
 
 import { NTB_CONTRACTS_SOURCE_GROUP } from '@modules/ntb/types';
-import { openNtbChart } from '@widgets/ntb/shared/services/openChart';
-
 import { downloadHistory } from '../../../../utils/downloadHistory';
+import { openContractsChart } from '../../../../utils/openContractsChart';
 import { useContextMenu } from '../useContextMenu';
 
 import type { ContractsTableDataItem } from '../../types';
 import type { ContextMenuItem } from '@uikit/ContextMenu/types';
 import type { MouseEvent } from 'react';
 
-jest.mock('@widgets/ntb/shared/services/openChart', () => ({ openNtbChart: jest.fn() }));
 jest.mock('../../../../utils/downloadHistory', () => ({ downloadHistory: jest.fn() }));
+jest.mock('../../../../utils/openContractsChart', () => ({ openContractsChart: jest.fn() }));
 
-const mockedOpenNtbChart = openNtbChart as jest.Mock;
+const mockedOpenContractsChart = openContractsChart as jest.Mock;
 const mockedDownloadHistory = downloadHistory as jest.Mock;
 
 const createRow = (partial: Partial<ContractsTableDataItem>): ContractsTableDataItem =>
   partial as ContractsTableDataItem;
 
-const rowWithKey = createRow({ productName: 'Пшеница', issKey: 'NTBVTFC:NTBVCT:11A12C22T33Z3' });
+const chartSymbols = [
+  { symbolId: 'NTBVTFC:NTBVCT:11A12C22T33Z3', symbol: 'до 50', symbolName: 'Пшеница, до 50' },
+  { symbolId: 'NTBVTFC:NTBVCT:11A12C22T33Z4', symbol: '50-100', symbolName: 'Пшеница, 50-100' },
+];
+
+const rowWithKey = createRow({ productName: 'Пшеница', issKey: 'NTBVTFC:NTBVCT:11A12C22T33Z3', chartSymbols });
 
 const createContextMenuEvent = () =>
   ({
@@ -48,14 +52,14 @@ describe('useContextMenu', () => {
     jest.clearAllMocks();
   });
 
-  it('открывает график по issKey строки', () => {
+  it('открывает график по набору символов строки', () => {
     const result = renderContextMenu(rowWithKey);
 
     act(() => {
       findItem(result.current.items, 'openChart')?.onClick?.();
     });
 
-    expect(mockedOpenNtbChart).toHaveBeenCalledWith('NTBVTFC:NTBVCT:11A12C22T33Z3');
+    expect(mockedOpenContractsChart).toHaveBeenCalledWith(chartSymbols);
   });
 
   it('не показывает пункт графика для строки без issKey', () => {
diff --git a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/hooks/useContextMenu.ts b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/hooks/useContextMenu.ts
index 8417c1de32c034cb27e06fb972d8d3d312479b31..3a46a941d6be6d13226483a809b3335e3b4dbc2d 100644
--- a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/hooks/useContextMenu.ts
+++ b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/hooks/useContextMenu.ts
@@ -1,7 +1,7 @@
 import { useContextMenuOverlay } from '@uikit/ContextMenuOverlay';
-import { openNtbChart } from '@widgets/ntb/shared/services/openChart';
 
 import { downloadHistory } from '../../../utils/downloadHistory';
+import { openContractsChart } from '../../../utils/openContractsChart';
 
 import type { ContractsTableDataItem } from '../types';
 import type { NTB_CONTRACTS_SOURCE_GROUP } from '@modules/ntb/types';
@@ -23,7 +23,13 @@ export const useContextMenu = ({ sourceGroup, onShowHistory }: UseContextMenuPro
 
   const items: ContextMenuItem[] = [
     ...(row?.issKey
-      ? [{ key: 'openChart', label: 'Отобразить в виджете График', onClick: () => openNtbChart(row.issKey) }]
+      ? [
+          {
+            key: 'openChart',
+            label: 'Отобразить в виджете График',
+            onClick: () => openContractsChart(row.chartSymbols),
+          },
+        ]
       : []),
     {
       key: 'showHistory',
diff --git a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/utils/__tests__/prepareContractsRows.test.ts b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/utils/__tests__/prepareContractsRows.test.ts
index dccbbf6cbe1e060c0582f6cc395a84caaf851597..a5e3c34ca115b71420c27c195d3328b263c56ca9 100644
--- a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/utils/__tests__/prepareContractsRows.test.ts
+++ b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/utils/__tests__/prepareContractsRows.test.ts
@@ -20,12 +20,23 @@ describe('prepareContractsRows', () => {
         range: '23.02-01.03.26',
         date: '27.02.2026',
         issKey: 'NTBVTFC:NTBVCT:11A12C22T33Z3',
+        chartSymbols: [
+          { symbolId: 'NTBVTFC:NTBVCT:11A12C22T33Z3', symbol: 'до 50', symbolName: 'Пшеница, до 50' },
+          { symbolId: 'NTBVTFC:NTBVCT:11A12C22T33Z4', symbol: '50-100', symbolName: 'Пшеница, 50-100' },
+        ],
         'до 50': 214,
         '50-100': 303,
       },
     ]);
   });
 
+  it('собирает символы графика по всем колонкам в порядке order', () => {
+    expect(prepareContractsRows([contract])?.[0].chartSymbols?.map((symbol) => symbol.symbolId)).toEqual([
+      'NTBVTFC:NTBVCT:11A12C22T33Z3',
+      'NTBVTFC:NTBVCT:11A12C22T33Z4',
+    ]);
+  });
+
   it('берёт issKey первой колонки по order', () => {
     expect(prepareContractsRows([contract])?.[0].issKey).toBe('NTBVTFC:NTBVCT:11A12C22T33Z3');
   });
diff --git a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/utils/prepareContractsRows.ts b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/utils/prepareContractsRows.ts
index bf8166bf0d5333467b509d6734e5a83987c760be..ae42723c62380f2e808174de1a6998604baec3dc 100644
--- a/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/utils/prepareContractsRows.ts
+++ b/src/widgets/ntb/AnalyticsContracts/components/ContractsTable/utils/prepareContractsRows.ts
@@ -1,7 +1,14 @@
 import { flattenColumnData, sortColumnData } from '../../../utils/columnData';
 
-import type { NormalizedContract } from '../../../types';
+import type { NormalizedContract, NormalizedContractColumnData } from '../../../types';
 import type { ContractsTableDataItem } from '../types';
+import type { ChartSymbol } from '@widgets/ntb/shared/types/chart';
+
+const toChartSymbol = (productName: string, { key, name }: NormalizedContractColumnData): ChartSymbol => ({
+  symbolId: key,
+  symbol: name,
+  symbolName: `${productName}, ${name}`,
+});
 
 export const prepareContractsRows = (
   contracts: NormalizedContract[] | undefined,
@@ -15,5 +22,6 @@ export const prepareContractsRows = (
       range,
       date,
       issKey: sortedColumnData[0]?.key ?? null,
+      chartSymbols: sortedColumnData.map((column) => toChartSymbol(productName, column)),
     };
   });
diff --git a/src/widgets/ntb/AnalyticsContracts/const.ts b/src/widgets/ntb/AnalyticsContracts/const.ts
index aa8905550c8de6882e8b99fbc70e75e16f84ee5c..0c8fb9f3dfce0ef9ab3e1abd836298d7fccf4d77 100644
--- a/src/widgets/ntb/AnalyticsContracts/const.ts
+++ b/src/widgets/ntb/AnalyticsContracts/const.ts
@@ -11,6 +11,8 @@ export const CONTRACTS_CHART_BOARD = 'NTBVCT';
 
 export const CONTRACTS_ISS_KEY_PREFIX = `${MARKET_SEGMENT_CODES.NTB_VTFC}:${CONTRACTS_CHART_BOARD}`;
 
+export const CONTRACTS_CHART_TIMEFRAME = '1w';
+
 export const DEFAULT_SOURCE_GROUP = NTB_CONTRACTS_SOURCE_GROUP.AVG;
 
 export const SOURCE_GROUP_TABS: Tab[] = [
diff --git a/src/widgets/ntb/AnalyticsContracts/properties/defaultProperties.ts b/src/widgets/ntb/AnalyticsContracts/properties/defaultProperties.ts
index 4f20609db7003105987ce89d7ac37193241da70f..471999504e334884211951c601c5db9535f438ed 100644
--- a/src/widgets/ntb/AnalyticsContracts/properties/defaultProperties.ts
+++ b/src/widgets/ntb/AnalyticsContracts/properties/defaultProperties.ts
@@ -4,6 +4,7 @@ import type { WidgetProperties } from './types';
 
 const DEFAULT_PROPERTIES: WidgetProperties = {
   selectedInstrument: null,
+  selectedChartSymbols: null,
   sourceGroup: DEFAULT_SOURCE_GROUP,
 };
 
diff --git a/src/widgets/ntb/AnalyticsContracts/properties/types.ts b/src/widgets/ntb/AnalyticsContracts/properties/types.ts
index 4159d86e411fb504c6fdd43f13316f8102b0d4d0..41842a71cec4861fa73b08fc3dc3068bf4054685 100644
--- a/src/widgets/ntb/AnalyticsContracts/properties/types.ts
+++ b/src/widgets/ntb/AnalyticsContracts/properties/types.ts
@@ -1,6 +1,8 @@
 import type { NTB_CONTRACTS_SOURCE_GROUP } from '@modules/ntb/types';
+import type { ChartSymbol } from '@widgets/ntb/shared/types/chart';
 
 export type WidgetProperties = {
   selectedInstrument: string | null;
+  selectedChartSymbols: ChartSymbol[] | null;
   sourceGroup: NTB_CONTRACTS_SOURCE_GROUP;
 };
diff --git a/src/widgets/ntb/AnalyticsContracts/types.ts b/src/widgets/ntb/AnalyticsContracts/types.ts
index 57bbcf4aac90d3949b71713baeb86c3ca20a437c..cb8b0709b1d04f696f2f81c2a042aaa5dc2a2c29 100644
--- a/src/widgets/ntb/AnalyticsContracts/types.ts
+++ b/src/widgets/ntb/AnalyticsContracts/types.ts
@@ -1,10 +1,12 @@
 import type { ResizableColumnType } from '@components/Table';
+import type { ChartSymbol } from '@widgets/ntb/shared/types/chart';
 
 export type ContractsRowStaticFields = {
   productName: string;
   range: string;
   date: string;
   issKey?: string | null;
+  chartSymbols?: ChartSymbol[];
 };
 
 export type ContractsRow = ContractsRowStaticFields & Record<string, unknown>;
diff --git a/src/widgets/ntb/AnalyticsContracts/utils/__tests__/openContractsChart.test.ts b/src/widgets/ntb/AnalyticsContracts/utils/__tests__/openContractsChart.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f6b4e44d66f935165071820e0f941bee93f88007
--- /dev/null
+++ b/src/widgets/ntb/AnalyticsContracts/utils/__tests__/openContractsChart.test.ts
@@ -0,0 +1,42 @@
+import { openNtbChart } from '@widgets/ntb/shared/services/openChart';
+
+import { openContractsChart } from '../openContractsChart';
+
+jest.mock('@widgets/ntb/shared/services/openChart', () => ({ openNtbChart: jest.fn() }));
+
+const mockedOpenNtbChart = openNtbChart as jest.Mock;
+
+const chartSymbols = [
+  { symbolId: 'NTBVTFC:NTBVCT:A', symbol: '≤ 180', symbolName: 'Пшеница, ≤ 180' },
+  { symbolId: 'NTBVTFC:NTBVCT:B', symbol: '(180, 190]', symbolName: 'Пшеница, (180, 190]' },
+  { symbolId: 'NTBVTFC:NTBVCT:C', symbol: '(190, 200]', symbolName: 'Пшеница, (190, 200]' },
+];
+
+describe('openContractsChart', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('открывает первую колонку основной серией, остальные сериями сравнения, таймфрейм неделя', () => {
+    openContractsChart(chartSymbols);
+
+    expect(mockedOpenNtbChart).toHaveBeenCalledWith('NTBVTFC:NTBVCT:A', {
+      symbol: '≤ 180',
+      symbolName: 'Пшеница, ≤ 180',
+      compareSymbols: chartSymbols.slice(1),
+      timeframe: '1w',
+    });
+  });
+
+  it('для одной колонки открывает график без серий сравнения', () => {
+    openContractsChart(chartSymbols.slice(0, 1));
+
+    expect(mockedOpenNtbChart).toHaveBeenCalledWith('NTBVTFC:NTBVCT:A', { compareSymbols: [], timeframe: '1w' });
+  });
+
+  it('без символов передаёт пустой инструмент, и график не открывается', () => {
+    openContractsChart(undefined);
+
+    expect(mockedOpenNtbChart).toHaveBeenCalledWith(undefined, { compareSymbols: [], timeframe: '1w' });
+  });
+});
diff --git a/src/widgets/ntb/AnalyticsContracts/utils/openContractsChart.ts b/src/widgets/ntb/AnalyticsContracts/utils/openContractsChart.ts
new file mode 100644
index 0000000000000000000000000000000000000000..10a780f0636a43e8d0ce7ad4b3ed664cf74d17dd
--- /dev/null
+++ b/src/widgets/ntb/AnalyticsContracts/utils/openContractsChart.ts
@@ -0,0 +1,16 @@
+import { openNtbChart } from '@widgets/ntb/shared/services/openChart';
+
+import { CONTRACTS_CHART_TIMEFRAME } from '../const';
+
+import type { ChartSymbol } from '@widgets/ntb/shared/types/chart';
+
+export const openContractsChart = (chartSymbols: ChartSymbol[] | undefined) => {
+  const [mainSymbol, ...compareSymbols] = chartSymbols ?? [];
+
+  openNtbChart(mainSymbol?.symbolId, {
+    symbol: mainSymbol?.symbol,
+    symbolName: mainSymbol?.symbolName,
+    compareSymbols,
+    timeframe: CONTRACTS_CHART_TIMEFRAME,
+  });
+};
diff --git a/src/widgets/ntb/shared/hooks/__tests__/useHandleRowClick.test.ts b/src/widgets/ntb/shared/hooks/__tests__/useHandleRowClick.test.ts
index c2554f330e3362f7b3bc2ae5f7ca972ed83f0979..338d8a50b8fbf737407f19139bbc1f82b6e1f142 100644
--- a/src/widgets/ntb/shared/hooks/__tests__/useHandleRowClick.test.ts
+++ b/src/widgets/ntb/shared/hooks/__tests__/useHandleRowClick.test.ts
@@ -23,6 +23,12 @@ const WIDGET_ID = 42;
 
 type DataRecord = { issKey: string | null };
 
+type ChartSymbols = { symbolId: string; symbol: string; symbolName: string }[];
+
+type RowWithChartSymbols = DataRecord & { chartSymbols: ChartSymbols };
+
+type BindableProperties = { selectedInstrument: string | null; selectedChartSymbols?: ChartSymbols | null };
+
 const createRow = (issKey: string | null): DataRecord => ({ issKey }) as Partial<DataRecord> as DataRecord;
 
 describe('useHandleRowClick', () => {
@@ -65,6 +71,39 @@ describe('useHandleRowClick', () => {
     expect(draft.selectedInstrument).toBe('ISS:1');
   });
 
+  it('пишет набор символов графика через getChartSymbols', () => {
+    const chartSymbols: ChartSymbols = [{ symbolId: 'ISS:1', symbol: 'A', symbolName: 'Продукт, A' }];
+    const { result } = renderHookWithProviders(
+      () =>
+        useHandleRowClick<RowWithChartSymbols, BindableProperties>({
+          getChartSymbols: (record) => record.chartSymbols,
+        }),
+      { mockDispatch: true },
+    );
+
+    act(() => {
+      result.current.handleRowClick({ issKey: 'ISS:1', chartSymbols });
+    });
+
+    const draft: BindableProperties = { selectedInstrument: null, selectedChartSymbols: null };
+    updateProperties.mock.calls[0][0](draft);
+
+    expect(draft.selectedChartSymbols).toEqual(chartSymbols);
+  });
+
+  it('не трогает набор символов графика, если getChartSymbols не передан', () => {
+    const { result } = renderHookWithProviders(() => useHandleRowClick(), { mockDispatch: true });
+
+    act(() => {
+      result.current.handleRowClick(createRow('ISS:1'));
+    });
+
+    const draft = { selectedInstrument: null, selectedChartSymbols: null };
+    updateProperties.mock.calls[0][0](draft);
+
+    expect(draft.selectedChartSymbols).toBeNull();
+  });
+
   it('подставляет null, если у строки нет issKey', () => {
     const { result, store } = renderHookWithProviders(() => useHandleRowClick(), { mockDispatch: true });
 
diff --git a/src/widgets/ntb/shared/hooks/useBindAndSaveInstrument.ts b/src/widgets/ntb/shared/hooks/useBindAndSaveInstrument.ts
index d940d7f920e098d95132a9088c566b43b2893aea..35ec77b8b09e98ce3d88ce267ee264b4fb1a35f5 100644
--- a/src/widgets/ntb/shared/hooks/useBindAndSaveInstrument.ts
+++ b/src/widgets/ntb/shared/hooks/useBindAndSaveInstrument.ts
@@ -6,7 +6,9 @@ import { addValuesToPublicContext } from '@store/slices/publicContext';
 import { markAsActiveMaster } from '@store/slices/widgets';
 import { useWidgetIdContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
 
-export const useBindAndSaveInstrument = <WP extends { selectedInstrument: string | null }>() => {
+import type { BindableProperties, ChartSymbol } from '../types/chart';
+
+export const useBindAndSaveInstrument = <WP extends BindableProperties>() => {
   const widgetId = useWidgetIdContext();
 
   const dispatch = useDispatch();
@@ -14,7 +16,7 @@ export const useBindAndSaveInstrument = <WP extends { selectedInstrument: string
   const { updateProperties } = useChangeProperties<WP>();
 
   const bindAndSaveInstrument = useCallback(
-    (issKey: string | undefined | null) => {
+    (issKey: string | undefined | null, chartSymbols?: ChartSymbol[]) => {
       dispatch(
         addValuesToPublicContext({
           id: widgetId,
@@ -29,6 +31,10 @@ export const useBindAndSaveInstrument = <WP extends { selectedInstrument: string
 
       updateProperties((state) => {
         state.selectedInstrument = issKey ?? null;
+
+        if (chartSymbols) {
+          state.selectedChartSymbols = chartSymbols;
+        }
       });
 
       dispatch(markAsActiveMaster({ widgetId }));
diff --git a/src/widgets/ntb/shared/hooks/useHandleRowClick.ts b/src/widgets/ntb/shared/hooks/useHandleRowClick.ts
index f3f972218a37552c9c8b57eb001febe1a33cbd80..5f0dd445f1880ad7851a9a82064b0f7b30975594 100644
--- a/src/widgets/ntb/shared/hooks/useHandleRowClick.ts
+++ b/src/widgets/ntb/shared/hooks/useHandleRowClick.ts
@@ -2,25 +2,31 @@ import { useCallback } from 'react';
 
 import { useBindAndSaveInstrument } from '@widgets/ntb/shared/hooks/useBindAndSaveInstrument';
 
+import type { BindableProperties, ChartSymbol } from '../types/chart';
+
 type UseHandleRowClickProps<T> = {
   /** Получает `issKey` из данных строки (record)\
    * Если не передан в качестве issKey по умолчанию используется `record.issKey`
    */
   getIssKey?: (record: T) => string | null | undefined;
+  /** Получает набор символов графика из данных строки (record)
+   * Мастер сохраняет его в properties, слейв-график открывает по нему серии сравнения
+   */
+  getChartSymbols?: (record: T) => ChartSymbol[] | undefined;
 };
 
 export const useHandleRowClick = <
   T extends { issKey?: string | null } & Record<string, unknown>,
-  WP extends { selectedInstrument: string | null },
->({ getIssKey }: UseHandleRowClickProps<T> = {}) => {
+  WP extends BindableProperties,
+>({ getIssKey, getChartSymbols }: UseHandleRowClickProps<T> = {}) => {
   const { bindAndSaveInstrument } = useBindAndSaveInstrument<WP>();
 
   const handleRowClick = useCallback(
     (record: T) => {
       const selectedInstrument = getIssKey?.(record) ?? record.issKey;
-      bindAndSaveInstrument(selectedInstrument);
+      bindAndSaveInstrument(selectedInstrument, getChartSymbols?.(record));
     },
-    [bindAndSaveInstrument, getIssKey],
+    [bindAndSaveInstrument, getChartSymbols, getIssKey],
   );
 
   return {
diff --git a/src/widgets/ntb/shared/services/__tests__/openChart.test.ts b/src/widgets/ntb/shared/services/__tests__/openChart.test.ts
index 4616313b7c75d836d15195427ca15a5a8ed4aa8d..19abe40721b693ebb10fecb89bbf7366249d749d 100644
--- a/src/widgets/ntb/shared/services/__tests__/openChart.test.ts
+++ b/src/widgets/ntb/shared/services/__tests__/openChart.test.ts
@@ -8,6 +8,8 @@ jest.mock('@widgets/Chart/creator', () => ({
 
 const mockedCreateGraphicWidget = createGraphicWidget as jest.Mock;
 
+const getSavedSnapshot = () => JSON.parse(mockedCreateGraphicWidget.mock.calls[0][0].moexChartState.savedData);
+
 describe('openNtbChart', () => {
   beforeEach(() => {
     jest.clearAllMocks();
@@ -27,4 +29,66 @@ describe('openNtbChart', () => {
 
     expect(mockedCreateGraphicWidget).not.toHaveBeenCalled();
   });
+
+  it('передаёт таймфрейм из опций', () => {
+    openNtbChart('NTBVTFC:NTBVCT:A', { timeframe: '1w' });
+
+    expect(mockedCreateGraphicWidget.mock.calls[0][0].moexChartState).toEqual({
+      timeframe: '1w',
+      initialInterval: '1Y',
+      initialChartSeriesType: 'Line',
+    });
+  });
+
+  it('кладёт серии сравнения в стартовый снапшот графика', () => {
+    const compareSymbols = [
+      { symbolId: 'NTBVTFC:NTBVCT:B', symbol: '(180, 190]', symbolName: 'Пшеница, (180, 190]' },
+      { symbolId: 'NTBVTFC:NTBVCT:C', symbol: '(190, 200]', symbolName: 'Пшеница, (190, 200]' },
+    ];
+
+    openNtbChart('NTBVTFC:NTBVCT:A', { compareSymbols, timeframe: '1w' });
+
+    expect(getSavedSnapshot()).toEqual({
+      charts: [
+        {
+          symbolId: 'NTBVTFC:NTBVCT:A',
+          timeframe: '1w',
+          chartSeriesType: 'Line',
+          panes: [
+            {
+              isMain: true,
+              id: 0,
+              drawings: [],
+              indicators: compareSymbols.map((symbolInfo) => ({ symbolInfo, seriesName: 'Line', scale: 'right' })),
+            },
+          ],
+        },
+      ],
+    });
+  });
+
+  it('передаёт имена основного инструмента в chartState и в снапшот', () => {
+    openNtbChart('NTBVTFC:NTBVCT:A', {
+      symbol: '≤ 10000',
+      symbolName: 'Ячмень, ≤ 10000',
+      compareSymbols: [{ symbolId: 'NTBVTFC:NTBVCT:B' }],
+    });
+
+    expect(mockedCreateGraphicWidget.mock.calls[0][0].chartState).toEqual({
+      savedInstrument: 'NTBVTFC:NTBVCT:A',
+      savedSymbol: '≤ 10000',
+      savedSymbolName: 'Ячмень, ≤ 10000',
+    });
+    expect(getSavedSnapshot().charts[0]).toMatchObject({
+      symbolId: 'NTBVTFC:NTBVCT:A',
+      symbol: '≤ 10000',
+      symbolName: 'Ячмень, ≤ 10000',
+    });
+  });
+
+  it('не добавляет снапшот без серий сравнения', () => {
+    openNtbChart('NTBVTFC:NTBVCT:A', { compareSymbols: [] });
+
+    expect(mockedCreateGraphicWidget.mock.calls[0][0].moexChartState.savedData).toBeUndefined();
+  });
 });
diff --git a/src/widgets/ntb/shared/services/openChart.ts b/src/widgets/ntb/shared/services/openChart.ts
index 2b4cf31f6f6c9d2eeb01018ecb192c47eb6b6aee..3c0ef218030627b4ffafc3afb65a230bd1eb96c3 100644
--- a/src/widgets/ntb/shared/services/openChart.ts
+++ b/src/widgets/ntb/shared/services/openChart.ts
@@ -1,22 +1,78 @@
 import { createGraphicWidget } from '@widgets/Chart/creator';
 
-import type { ChartSeriesType } from '@mx-charts/charts';
+import type { ChartSymbol } from '../types/chart';
+import type { ChartSeriesType, Intervals, MoexChartSnapshotInput, Timeframes } from '@mx-charts/charts';
+
+type ChartSnapshotInput = MoexChartSnapshotInput['charts'][number];
+type PaneSnapshotInput = ChartSnapshotInput['panes'][number];
+type CompareSnapshotInput = Extract<PaneSnapshotInput['indicators'][number], { symbolInfo: ChartSymbol }>;
+
+type MainSymbolNames = Pick<ChartSymbol, 'symbol' | 'symbolName'>;
+
+type OpenNtbChartOptions = MainSymbolNames & {
+  compareSymbols?: ChartSymbol[];
+  timeframe?: `${Timeframes}`;
+};
 
 const NTB_CHART_SERIES_TYPE: ChartSeriesType = 'Line';
+const NTB_CHART_TIMEFRAME: `${Timeframes}` = '1d';
+const NTB_CHART_INTERVAL: `${Intervals}` = '1Y';
+const MAIN_PANE_ID = 0;
+const COMPARE_SCALE = 'right' as CompareSnapshotInput['scale'];
+
+const createCompareSnapshot = (symbolInfo: ChartSymbol): CompareSnapshotInput => ({
+  symbolInfo,
+  seriesName: NTB_CHART_SERIES_TYPE,
+  scale: COMPARE_SCALE,
+});
 
-export const openNtbChart = (instrumentId: string | null | undefined) => {
+const createSnapshot = (
+  mainSymbol: ChartSymbol,
+  timeframe: `${Timeframes}`,
+  compareSymbols: ChartSymbol[],
+): MoexChartSnapshotInput => ({
+  charts: [
+    {
+      ...mainSymbol,
+      timeframe: timeframe as Timeframes,
+      chartSeriesType: NTB_CHART_SERIES_TYPE,
+      panes: [
+        {
+          isMain: true,
+          id: MAIN_PANE_ID,
+          indicators: compareSymbols.map(createCompareSnapshot),
+          drawings: [],
+        },
+      ],
+    },
+  ],
+});
+
+export const openNtbChart = (
+  instrumentId: string | null | undefined,
+  { compareSymbols = [], timeframe = NTB_CHART_TIMEFRAME, symbol, symbolName }: OpenNtbChartOptions = {},
+) => {
   if (!instrumentId) {
     return;
   }
 
+  const mainSymbol: ChartSymbol = { symbolId: instrumentId, symbol, symbolName };
+
+  const savedData = compareSymbols.length
+    ? JSON.stringify(createSnapshot(mainSymbol, timeframe, compareSymbols))
+    : undefined;
+
   createGraphicWidget({
     chartState: {
       savedInstrument: instrumentId,
+      ...(symbol ? { savedSymbol: symbol } : {}),
+      ...(symbolName ? { savedSymbolName: symbolName } : {}),
     },
     moexChartState: {
-      timeframe: '1d',
-      initialInterval: '1Y',
+      timeframe,
+      initialInterval: NTB_CHART_INTERVAL,
       initialChartSeriesType: NTB_CHART_SERIES_TYPE,
+      ...(savedData ? { savedData } : {}),
     },
   });
 };
diff --git a/src/widgets/ntb/shared/types/chart.ts b/src/widgets/ntb/shared/types/chart.ts
new file mode 100644
index 0000000000000000000000000000000000000000..22eeecc7f28caf3bdf6517a4969f3f6f21975398
--- /dev/null
+++ b/src/widgets/ntb/shared/types/chart.ts
@@ -0,0 +1,10 @@
+export type ChartSymbol = {
+  symbolId: string;
+  symbol?: string;
+  symbolName?: string;
+};
+
+export type BindableProperties = {
+  selectedInstrument: string | null;
+  selectedChartSymbols?: ChartSymbol[] | null;
+};