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


diff --git a/src/modules/ntb/utils/__tests__/registerNtbBarsResolver.test.ts b/src/modules/ntb/utils/__tests__/registerNtbBarsResolver.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b6a415d34681148b9ba18910c503cccee4c4cd45
--- /dev/null
+++ b/src/modules/ntb/utils/__tests__/registerNtbBarsResolver.test.ts
@@ -0,0 +1,120 @@
+import { registerBarsResolver } from '@widgets/Chart/requestBars';
+
+import { registerNtbBarsResolver } from '../registerNtbBarsResolver';
+
+import type { NtbBarPoint } from '../registerNtbBarsResolver';
+import type { Candle } from 'moex-chart';
+
+jest.mock('@widgets/Chart/requestBars', () => ({
+  registerBarsResolver: jest.fn(),
+}));
+
+const mockRegisterBarsResolver = registerBarsResolver as jest.Mock;
+
+type BarsResolver = (args: { ticker?: string }) => Promise<Candle[]>;
+
+const registerAndGetResolver = (fetchBars: (securityId: string) => Promise<NtbBarPoint[]>): BarsResolver => {
+  registerNtbBarsResolver('TEST_BOARD', fetchBars);
+
+  return mockRegisterBarsResolver.mock.calls[0][1] as BarsResolver;
+};
+
+describe('registerNtbBarsResolver', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('регистрирует резолвер под переданный борд', () => {
+    registerNtbBarsResolver('TEST_BOARD', jest.fn());
+
+    expect(mockRegisterBarsResolver).toHaveBeenCalledWith('TEST_BOARD', expect.any(Function));
+  });
+
+  it('достаёт securityId из тикера (3-й сегмент) и передаёт его в fetchBars', async () => {
+    const fetchBars = jest.fn().mockResolvedValue([]);
+    const resolver = registerAndGetResolver(fetchBars);
+
+    await resolver({ ticker: 'PREFIX:BOARD:SEC123' });
+
+    expect(fetchBars).toHaveBeenCalledWith('SEC123');
+  });
+
+  it('маппит точки в плоские свечи: value → OHLC, date → время, volume', async () => {
+    const resolver = registerAndGetResolver(async () => [{ date: '2026-05-19', value: 40, volume: 5 }]);
+
+    const bars = await resolver({ ticker: 'P:B:SEC' });
+
+    expect(bars).toEqual([{ time: Date.parse('2026-05-19'), open: 40, high: 40, low: 40, close: 40, volume: 5 }]);
+  });
+
+  it('подставляет volume=0, если объёма нет', async () => {
+    const resolver = registerAndGetResolver(async () => [{ date: '2026-05-19', value: 40 }]);
+
+    const bars = await resolver({ ticker: 'P:B:SEC' });
+
+    expect(bars[0].volume).toBe(0);
+  });
+
+  it('сортирует бары по возрастанию времени', async () => {
+    const resolver = registerAndGetResolver(async () => [
+      { date: '2026-05-20', value: 2 },
+      { date: '2026-05-19', value: 1 },
+    ]);
+
+    const bars = await resolver({ ticker: 'P:B:SEC' });
+
+    expect(bars.map((bar) => bar.time)).toEqual([Date.parse('2026-05-19'), Date.parse('2026-05-20')]);
+  });
+
+  it('схлопывает дубли по дате — побеждает последнее значение', async () => {
+    const resolver = registerAndGetResolver(async () => [
+      { date: '2026-05-19', value: 10 },
+      { date: '2026-05-19', value: 99 },
+    ]);
+
+    const bars = await resolver({ ticker: 'P:B:SEC' });
+
+    expect(bars).toHaveLength(1);
+    expect(bars[0].close).toBe(99);
+  });
+
+  it('пропускает точки без числового value или без даты', async () => {
+    const resolver = registerAndGetResolver(async () => [
+      { date: '2026-05-19' },
+      { date: '', value: 5 },
+      { date: '2026-05-20', value: 7 },
+    ]);
+
+    const bars = await resolver({ ticker: 'P:B:SEC' });
+
+    expect(bars).toEqual([{ time: Date.parse('2026-05-20'), open: 7, high: 7, low: 7, close: 7, volume: 0 }]);
+  });
+
+  it('парсит дату в формате dd.mm.yyyy через fallback', async () => {
+    const resolver = registerAndGetResolver(async () => [{ date: '15.03.2025', value: 3 }]);
+
+    const bars = await resolver({ ticker: 'P:B:SEC' });
+
+    expect(bars[0].time).toBe(Date.UTC(2025, 2, 15));
+  });
+
+  it('возвращает [] и не вызывает fetchBars, если securityId в тикере нет', async () => {
+    const fetchBars = jest.fn();
+    const resolver = registerAndGetResolver(fetchBars);
+
+    const bars = await resolver({ ticker: 'PREFIX:BOARD' });
+
+    expect(bars).toEqual([]);
+    expect(fetchBars).not.toHaveBeenCalled();
+  });
+
+  it('возвращает [] при undefined ticker', async () => {
+    const fetchBars = jest.fn();
+    const resolver = registerAndGetResolver(fetchBars);
+
+    const bars = await resolver({ ticker: undefined });
+
+    expect(bars).toEqual([]);
+    expect(fetchBars).not.toHaveBeenCalled();
+  });
+});
diff --git a/src/modules/ntb/utils/registerNtbBarsResolver.ts b/src/modules/ntb/utils/registerNtbBarsResolver.ts
new file mode 100644
index 0000000000000000000000000000000000000000..338eed7cd7986e45ec528f985053e66ead3774a1
--- /dev/null
+++ b/src/modules/ntb/utils/registerNtbBarsResolver.ts
@@ -0,0 +1,56 @@
+import { registerBarsResolver } from '@widgets/Chart/requestBars';
+
+import type { Candle } from 'moex-chart';
+
+export type NtbBarPoint = {
+  date?: string;
+  value?: number;
+  volume?: number;
+};
+
+export type FetchNtbBars = (securityId: string) => Promise<NtbBarPoint[]>;
+
+const parseChartDate = (date: string): number => {
+  const parsed = Date.parse(date);
+  if (!Number.isNaN(parsed)) {
+    return parsed;
+  }
+
+  const [day, month, fullYear] = date.split('.');
+  return Date.UTC(Number(fullYear), Number(month) - 1, Number(day));
+};
+
+const mapPointsToFlatBars = (points: NtbBarPoint[]): Candle[] => {
+  const barByTime = new Map<number, Candle>();
+
+  points.forEach((point) => {
+    if (typeof point.value === 'number' && point.date) {
+      const time = parseChartDate(point.date);
+
+      barByTime.set(time, {
+        time,
+        open: point.value,
+        high: point.value,
+        low: point.value,
+        close: point.value,
+        volume: point.volume ?? 0,
+      });
+    }
+  });
+
+  return Array.from(barByTime.values()).sort((a, b) => a.time - b.time);
+};
+
+export const registerNtbBarsResolver = (board: string, fetchBars: FetchNtbBars) => {
+  registerBarsResolver(board, async ({ ticker }) => {
+    const securityId = ticker?.split(/[:.]/)[2];
+
+    if (!securityId) {
+      return [];
+    }
+
+    const points = await fetchBars(securityId);
+
+    return mapPointsToFlatBars(points);
+  });
+};
diff --git a/src/widgets/Chart/__tests__/useMoexchart.test.tsx b/src/widgets/Chart/__tests__/useMoexchart.test.tsx
index 9063648308bac7a44182838b9f21852251111b90..2fd134c57c5a810a91e6816943804ffdecd2c233 100644
--- a/src/widgets/Chart/__tests__/useMoexchart.test.tsx
+++ b/src/widgets/Chart/__tests__/useMoexchart.test.tsx
@@ -85,6 +85,7 @@ interface MockMoexChartConfig {
 
 interface MockMoexChartState {
   tf?: TimeframeValue;
+  initialInterval?: string;
   savedData?: string;
 }
 
@@ -113,6 +114,7 @@ describe('useMoexChart', () => {
   const mockSetSnapshot = jest.fn();
   const mockSetSymbol = jest.fn();
   const mockGetCompareManager = jest.fn();
+  const mockSetSettings = jest.fn();
 
   const mockDataSource = jest.fn();
   const mockStartRealtime = jest.fn();
@@ -200,6 +202,7 @@ describe('useMoexChart', () => {
         setSnapshot: mockSetSnapshot,
         setSymbol: mockSetSymbol,
         getCompareManager: mockGetCompareManager,
+        setSettings: mockSetSettings,
       };
     });
   });
@@ -530,4 +533,55 @@ describe('useMoexChart', () => {
     // Assert
     expect(mockDestroy).toHaveBeenCalledTimes(1);
   });
+  it('should apply initial interval on chart creation', () => {
+    // Arrange
+    mockMoexChartState = {
+      tf: Timeframes['1m'],
+      savedData: undefined,
+      initialInterval: '1Y',
+    };
+    // Act
+    render(<TestComponent symbol="MOEX:SBER" />);
+    // Assert
+    expect(mockSetSettings).toHaveBeenCalledWith({ interval: '1Y' });
+  });
+  it('should apply initial interval even when saved data exists', () => {
+    // Arrange
+    mockMoexChartState = {
+      tf: Timeframes['5m'],
+      savedData: JSON.stringify(mockSnapshot),
+      initialInterval: '1Y',
+    };
+    // Act
+    render(<TestComponent symbol="MOEX:SBER" />);
+    // Assert
+    expect(mockSetSettings).toHaveBeenCalledWith({ interval: '1Y' });
+  });
+  it('should clear initial interval in widget properties after applying', () => {
+    // Arrange
+    mockMoexChartState = {
+      tf: Timeframes['1m'],
+      savedData: undefined,
+      initialInterval: '1Y',
+    };
+    // Act
+    render(<TestComponent symbol="MOEX:SBER" />);
+    // Assert
+    expect(mockUpdateProperties).toHaveBeenCalledTimes(1);
+    const updateCallback = mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;
+    const mockState: MockPropertiesState = {
+      moexChartState: {
+        tf: Timeframes['1m'],
+        initialInterval: '1Y',
+      },
+    };
+    updateCallback(mockState);
+    expect(mockState.moexChartState?.initialInterval).toBeUndefined();
+  });
+  it('should not apply interval when initial interval is not set', () => {
+    // Arrange
+    render(<TestComponent symbol="MOEX:SBER" />);
+    // Assert
+    expect(mockSetSettings).not.toHaveBeenCalled();
+  });
 });
diff --git a/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts b/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
index 74691369be3ba8f69da4aa3af6f39953120c4376..45cd44ee32113bb76ae6c8355665791c7967bf2e 100644
--- a/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
+++ b/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
@@ -1,6 +1,6 @@
 import isNil from 'lodash/isNil';
 
-import { MoexChart, Timeframes } from 'moex-chart';
+import { Intervals, MoexChart, Timeframes } from 'moex-chart';
 
 import { useEffect, useRef, useState } from 'react';
 
@@ -34,6 +34,7 @@ export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) =>
 
   const timeframeRef = useRef<Timeframes | undefined>(moexChartState?.tf);
   const savedDataRef = useRef<string | undefined>(moexChartState?.savedData);
+  const initialIntervalRef = useRef<Intervals | undefined>(moexChartState?.initialInterval);
   const updateTimeframeRef = useRef<((tf: Timeframes) => void) | null>(null);
 
   useEffect(() => {
@@ -170,6 +171,19 @@ export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) =>
 
     chartRef.current = chart;
     compareManagerRef.current = chart.getCompareManager();
+
+    if (initialIntervalRef.current) {
+      chart.setSettings({ interval: initialIntervalRef.current });
+
+      updateProperties((state) => {
+        state.moexChartState = {
+          ...state.moexChartState,
+          initialInterval: undefined,
+        };
+      });
+      initialIntervalRef.current = undefined;
+    }
+
     const intervalId = setInterval(() => {
       saveSnapshot();
     }, 1000);
diff --git a/src/widgets/Chart/properties/types.ts b/src/widgets/Chart/properties/types.ts
index 9d0e36c23b9b8116f0a5e668ee73110cf9a1e303..1342b8cb0b3d1bdb27018fc6ab17d83fe77677dc 100644
--- a/src/widgets/Chart/properties/types.ts
+++ b/src/widgets/Chart/properties/types.ts
@@ -2,7 +2,7 @@ import { Contract } from '@modules/contracts/types';
 
 import { ChartIndicativeData } from '../types';
 
-import type { Timeframes } from 'moex-chart';
+import type { Intervals, Timeframes } from 'moex-chart';
 
 export type WidgetProperties = {
   chartState: {
@@ -12,7 +12,8 @@ export type WidgetProperties = {
   };
   indicativeData?: ChartIndicativeData;
   moexChartState?: {
-    tf: Timeframes;
+    tf?: Timeframes;
+    initialInterval?: Intervals;
     savedData?: string;
   };
 };
diff --git a/src/widgets/ntb/Indexes/components/IndexesTable/hooks/useContextMenu.ts b/src/widgets/ntb/Indexes/components/IndexesTable/hooks/useContextMenu.ts
index 0be9e4e504570390f3c53bd53f5ed3392b090387..067708e69160f29871ec038e3e6e8145edc79a30 100644
--- a/src/widgets/ntb/Indexes/components/IndexesTable/hooks/useContextMenu.ts
+++ b/src/widgets/ntb/Indexes/components/IndexesTable/hooks/useContextMenu.ts
@@ -25,6 +25,10 @@ export const useContextMenu = ({ onShowHistory }: UseContextMenuProps) => {
         chartState: {
           savedInstrument: instrument.issKey,
         },
+        moexChartState: {
+          tf: '1d',
+          initialInterval: '1Y',
+        },
       });
     }
   };
diff --git a/src/widgets/ntb/Indexes/components/IndexesTable/hooks/useTableData.ts b/src/widgets/ntb/Indexes/components/IndexesTable/hooks/useTableData.ts
index 4b82647288ba26265676da51d21f45e86c3b2d49..0674e045f158e5ebcd0085e18708b52aa22e971c 100644
--- a/src/widgets/ntb/Indexes/components/IndexesTable/hooks/useTableData.ts
+++ b/src/widgets/ntb/Indexes/components/IndexesTable/hooks/useTableData.ts
@@ -17,6 +17,7 @@ export const useTableData = (filters?: GetCommodityIndexesRequest) => {
   const { issIndexes, ntbIndexes } = useMemo(() => splitIndexesBySource(indexesData), [indexesData]);
 
   const indexesWithContracts = useIndexesWithContracts(issIndexes);
+  const ntbIndexesWithContracts = useIndexesWithContracts(ntbIndexes);
 
   const { quotesMap } = useQuotes(indexesWithContracts);
 
@@ -25,7 +26,7 @@ export const useTableData = (filters?: GetCommodityIndexesRequest) => {
     [indexesWithContracts, quotesMap],
   );
 
-  const ntbData = useIndexesWithMarketData(ntbIndexes);
+  const ntbData = useIndexesWithMarketData(ntbIndexesWithContracts);
 
   const resultData = useMemo<IndexesTableDataItem[] | undefined>(() => {
     if (!indexesData) {
diff --git a/src/widgets/ntb/Indexes/const.ts b/src/widgets/ntb/Indexes/const.ts
index 58d0ea23633b69967795d9542540b5d88668fd50..a768aaf3c04df333e1b917eb13179dc1bb6165a6 100644
--- a/src/widgets/ntb/Indexes/const.ts
+++ b/src/widgets/ntb/Indexes/const.ts
@@ -3,3 +3,5 @@ export enum TableKeys {
   History = 'history',
   NtbHistory = 'ntbHistory',
 }
+
+export const INDEXES_CHART_BOARD = 'AGRO';
diff --git a/src/widgets/ntb/Indexes/hooks/__tests__/useRegisterIndexesChartSource.test.ts b/src/widgets/ntb/Indexes/hooks/__tests__/useRegisterIndexesChartSource.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1bee9b0db98cdddd97873eac536e2dd8b1263139
--- /dev/null
+++ b/src/widgets/ntb/Indexes/hooks/__tests__/useRegisterIndexesChartSource.test.ts
@@ -0,0 +1,30 @@
+import { renderHook } from '@testing-library/react';
+
+import { registerIndexesChartSource } from '../../utils/indexesChartDataSource';
+import { useRegisterIndexesChartSource } from '../useRegisterIndexesChartSource';
+
+jest.mock('../../utils/indexesChartDataSource', () => ({
+  registerIndexesChartSource: jest.fn(),
+}));
+
+const mockRegister = registerIndexesChartSource as jest.Mock;
+
+describe('useRegisterIndexesChartSource', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('регистрирует источник баров один раз при монтировании', () => {
+    renderHook(() => useRegisterIndexesChartSource());
+
+    expect(mockRegister).toHaveBeenCalledTimes(1);
+  });
+
+  it('не регистрирует повторно при ре-рендере', () => {
+    const { rerender } = renderHook(() => useRegisterIndexesChartSource());
+
+    rerender();
+
+    expect(mockRegister).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/src/widgets/ntb/Indexes/hooks/useRegisterIndexesChartSource.ts b/src/widgets/ntb/Indexes/hooks/useRegisterIndexesChartSource.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e8279c46cc842947d709d22c99a094b1a994e80
--- /dev/null
+++ b/src/widgets/ntb/Indexes/hooks/useRegisterIndexesChartSource.ts
@@ -0,0 +1,9 @@
+import { useEffect } from 'react';
+
+import { registerIndexesChartSource } from '../utils/indexesChartDataSource';
+
+export const useRegisterIndexesChartSource = () => {
+  useEffect(() => {
+    registerIndexesChartSource();
+  }, []);
+};
diff --git a/src/widgets/ntb/Indexes/utils/__tests__/indexesChartDataSource.test.ts b/src/widgets/ntb/Indexes/utils/__tests__/indexesChartDataSource.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fd986131583a1a489b841cd51f9d9ca101052f99
--- /dev/null
+++ b/src/widgets/ntb/Indexes/utils/__tests__/indexesChartDataSource.test.ts
@@ -0,0 +1,57 @@
+import { ntbIndexesController } from '@api/controllers/ntbController';
+import { registerNtbBarsResolver } from '@modules/ntb/utils/registerNtbBarsResolver';
+
+import { INDEXES_CHART_BOARD } from '../../const';
+import { registerIndexesChartSource } from '../indexesChartDataSource';
+
+import type { NtbBarPoint } from '@modules/ntb/utils/registerNtbBarsResolver';
+
+jest.mock('@modules/ntb/utils/registerNtbBarsResolver', () => ({
+  registerNtbBarsResolver: jest.fn(),
+}));
+
+jest.mock('@api/controllers/ntbController', () => ({
+  ntbIndexesController: {
+    getNtbHistory: jest.fn(),
+  },
+}));
+
+const mockRegisterNtbBarsResolver = registerNtbBarsResolver as jest.Mock;
+const mockGetNtbHistory = ntbIndexesController.getNtbHistory as jest.Mock;
+
+type FetchBars = (securityId: string) => Promise<NtbBarPoint[]>;
+
+const getFetcher = (): FetchBars => {
+  registerIndexesChartSource();
+
+  return mockRegisterNtbBarsResolver.mock.calls[0][1] as FetchBars;
+};
+
+describe('indexesChartDataSource', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('регистрирует источник баров под борд AGRO', () => {
+    registerIndexesChartSource();
+
+    expect(mockRegisterNtbBarsResolver).toHaveBeenCalledWith(INDEXES_CHART_BOARD, expect.any(Function));
+  });
+
+  it('запрашивает NTB-историю по securityId и маппит price/date/volume в точки', async () => {
+    mockGetNtbHistory.mockResolvedValue({ data: { history: [{ price: 40, date: '2026-05-19', volume: 5 }] } });
+    const fetchBars = getFetcher();
+
+    const points = await fetchBars('SEC123');
+
+    expect(mockGetNtbHistory).toHaveBeenCalledWith({ securityId: 'SEC123' });
+    expect(points).toEqual([{ date: '2026-05-19', value: 40, volume: 5 }]);
+  });
+
+  it('возвращает [] при отсутствующей истории', async () => {
+    mockGetNtbHistory.mockResolvedValue({ data: null });
+    const fetchBars = getFetcher();
+
+    expect(await fetchBars('SEC123')).toEqual([]);
+  });
+});
diff --git a/src/widgets/ntb/Indexes/utils/indexesChartDataSource.ts b/src/widgets/ntb/Indexes/utils/indexesChartDataSource.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19df9c6ae13d5723e6d48cb1f547912c6ccc2eff
--- /dev/null
+++ b/src/widgets/ntb/Indexes/utils/indexesChartDataSource.ts
@@ -0,0 +1,12 @@
+import { ntbIndexesController } from '@api/controllers/ntbController';
+import { registerNtbBarsResolver } from '@modules/ntb/utils/registerNtbBarsResolver';
+
+import { INDEXES_CHART_BOARD } from '../const';
+
+export const registerIndexesChartSource = () => {
+  registerNtbBarsResolver(INDEXES_CHART_BOARD, async (securityId) => {
+    const { data } = await ntbIndexesController.getNtbHistory({ securityId });
+
+    return (data?.history ?? []).map((item) => ({ date: item.date, value: item.price, volume: item.volume }));
+  });
+};
diff --git a/src/widgets/ntb/Indexes/widget.tsx b/src/widgets/ntb/Indexes/widget.tsx
index 04cc74739124012bcf931b0e2fc26fe9b8e81e0e..d2b5421db8b816670540b912d04666a6e6c75ba8 100644
--- a/src/widgets/ntb/Indexes/widget.tsx
+++ b/src/widgets/ntb/Indexes/widget.tsx
@@ -10,6 +10,7 @@ import { HistoryTable } from './components/HistoryTable';
 import { IndexesTable } from './components/IndexesTable';
 import { useColumnsSettings } from './hooks/useColumnsSettings';
 import { useFilterSwitch } from './hooks/useFilterSwitch';
+import { useRegisterIndexesChartSource } from './hooks/useRegisterIndexesChartSource';
 import { useShowHstory } from './hooks/useShowHistory';
 import styles from './Indexes.module.scss';
 import { downloadHistory } from './utils/downloadHistory';
@@ -17,6 +18,8 @@ import { downloadHistory } from './utils/downloadHistory';
 import type { WidgetContentBasicProps } from 'types/Widgets';
 
 const NtbIndexes: FC<WidgetContentBasicProps> = (props) => {
+  useRegisterIndexesChartSource();
+
   const filterSwitch = useFilterSwitch();
 
   const { historySecId, historySourceId, handleShowHistory, handleReturnFromHistory } = useShowHstory();
diff --git a/src/widgets/ntb/Logistic/LogisticContextMenu/__tests__/useContextMenu.test.ts b/src/widgets/ntb/Logistic/LogisticContextMenu/__tests__/useContextMenu.test.ts
index 5b2c68104d95db544d524b744ec6c6c4441b7d26..b7ad8f6bc8b64782abb35791b8d4edca29caad41 100644
--- a/src/widgets/ntb/Logistic/LogisticContextMenu/__tests__/useContextMenu.test.ts
+++ b/src/widgets/ntb/Logistic/LogisticContextMenu/__tests__/useContextMenu.test.ts
@@ -133,7 +133,7 @@ describe('useContextMenu', () => {
 
       expect(mockedCreateGraphicWidget).toHaveBeenCalledWith({
         chartState: { savedInstrument: 'NTBVTFC.NTBVLB.111222333' },
-        moexChartState: { tf: '1d' },
+        moexChartState: { tf: '1d', initialInterval: '1Y' },
       });
       expect(mockProps.setOpenContextMenu).toHaveBeenCalledWith(false);
     });
diff --git a/src/widgets/ntb/Logistic/LogisticContextMenu/useContextMenu.ts b/src/widgets/ntb/Logistic/LogisticContextMenu/useContextMenu.ts
index 396d56419fc01b2673d6546b50936f925007c93e..6220780e91387a340f8b1c6fffa4eec4ef250c07 100644
--- a/src/widgets/ntb/Logistic/LogisticContextMenu/useContextMenu.ts
+++ b/src/widgets/ntb/Logistic/LogisticContextMenu/useContextMenu.ts
@@ -34,7 +34,7 @@ export const useContextMenu = ({
     if (issKey) {
       createGraphicWidget({
         chartState: { savedInstrument: issKey },
-        moexChartState: { tf: '1d' },
+        moexChartState: { tf: '1d', initialInterval: '1Y' },
       });
     }
     setOpenContextMenu(false);
diff --git a/src/widgets/ntb/Logistic/utils/__tests__/logisticChartDataSource.test.ts b/src/widgets/ntb/Logistic/utils/__tests__/logisticChartDataSource.test.ts
index 54684e1bcfeef47bb67768f190909c7dacc33c6c..9297b7f3c02f27be7cc9f48e8227a44d3f05e939 100644
--- a/src/widgets/ntb/Logistic/utils/__tests__/logisticChartDataSource.test.ts
+++ b/src/widgets/ntb/Logistic/utils/__tests__/logisticChartDataSource.test.ts
@@ -1,14 +1,13 @@
 import { ntbLogisticController } from '@api/controllers/ntbController';
-import { registerBarsResolver } from '@widgets/Chart/requestBars';
+import { registerNtbBarsResolver } from '@modules/ntb/utils/registerNtbBarsResolver';
 
 import { LOGISTIC_CHART_BOARD } from '../../constants';
 import { registerLogisticChartSource } from '../logisticChartDataSource';
 
-import type { Logistic } from '@modules/ntb/types';
-import type { Candle } from 'moex-chart';
+import type { NtbBarPoint } from '@modules/ntb/utils/registerNtbBarsResolver';
 
-jest.mock('@widgets/Chart/requestBars', () => ({
-  registerBarsResolver: jest.fn(),
+jest.mock('@modules/ntb/utils/registerNtbBarsResolver', () => ({
+  registerNtbBarsResolver: jest.fn(),
 }));
 
 jest.mock('@api/controllers/ntbController', () => ({
@@ -17,131 +16,42 @@ jest.mock('@api/controllers/ntbController', () => ({
   },
 }));
 
-const mockRegisterBarsResolver = registerBarsResolver as jest.Mock;
+const mockRegisterNtbBarsResolver = registerNtbBarsResolver as jest.Mock;
 const mockGetLogisticHistory = ntbLogisticController.getLogisticHistory as jest.Mock;
 
-type BarsResolver = (args: { ticker?: string }) => Promise<Candle[]>;
+type FetchBars = (securityId: string) => Promise<NtbBarPoint[]>;
 
-const getRegisteredResolver = (): BarsResolver => {
+const getFetcher = (): FetchBars => {
   registerLogisticChartSource();
 
-  return mockRegisterBarsResolver.mock.calls[0][1] as BarsResolver;
+  return mockRegisterNtbBarsResolver.mock.calls[0][1] as FetchBars;
 };
 
-const createHistoryItem = (partial: Partial<Logistic>): Logistic => partial as Logistic;
-
 describe('logisticChartDataSource', () => {
   beforeEach(() => {
     jest.clearAllMocks();
   });
 
-  it('регистрирует резолвер под борд NTBVLB', () => {
+  it('регистрирует источник баров под борд NTBVLB', () => {
     registerLogisticChartSource();
 
-    expect(mockRegisterBarsResolver).toHaveBeenCalledWith(LOGISTIC_CHART_BOARD, expect.any(Function));
+    expect(mockRegisterNtbBarsResolver).toHaveBeenCalledWith(LOGISTIC_CHART_BOARD, expect.any(Function));
   });
 
-  describe('резолвер', () => {
-    it('достаёт securityId из тикера и запрашивает по нему историю', async () => {
-      mockGetLogisticHistory.mockResolvedValue({ data: [] });
-      const resolver = getRegisteredResolver();
-
-      await resolver({ ticker: 'NTBVTFC:NTBVLB:SEC123' });
-
-      expect(mockGetLogisticHistory).toHaveBeenCalledWith({ securityId: 'SEC123' });
-    });
-
-    it('маппит историю в бары: Тариф → цена, Дата → время', async () => {
-      mockGetLogisticHistory.mockResolvedValue({
-        data: [createHistoryItem({ value: 40, valueDate: '2026-05-19' })],
-      });
-      const resolver = getRegisteredResolver();
-
-      const bars = await resolver({ ticker: 'NTBVTFC:NTBVLB:SEC123' });
-
-      expect(bars).toEqual([{ time: Date.parse('2026-05-19'), open: 40, high: 40, low: 40, close: 40, volume: 0 }]);
-    });
-
-    it('сортирует бары по возрастанию времени', async () => {
-      mockGetLogisticHistory.mockResolvedValue({
-        data: [
-          createHistoryItem({ value: 2, valueDate: '2026-05-20' }),
-          createHistoryItem({ value: 1, valueDate: '2026-05-19' }),
-        ],
-      });
-      const resolver = getRegisteredResolver();
-
-      const bars = await resolver({ ticker: 'NTBVTFC:NTBVLB:SEC123' });
-
-      expect(bars.map((bar) => bar.time)).toEqual([Date.parse('2026-05-19'), Date.parse('2026-05-20')]);
-    });
-
-    it('схлопывает дубли по дате — побеждает последнее значение', async () => {
-      mockGetLogisticHistory.mockResolvedValue({
-        data: [
-          createHistoryItem({ value: 10, valueDate: '2026-05-19' }),
-          createHistoryItem({ value: 99, valueDate: '2026-05-19' }),
-        ],
-      });
-      const resolver = getRegisteredResolver();
-
-      const bars = await resolver({ ticker: 'NTBVTFC:NTBVLB:SEC123' });
-
-      expect(bars).toHaveLength(1);
-      expect(bars[0].close).toBe(99);
-    });
+  it('запрашивает историю по securityId и маппит value/valueDate в точки', async () => {
+    mockGetLogisticHistory.mockResolvedValue({ data: [{ value: 40, valueDate: '2026-05-19' }] });
+    const fetchBars = getFetcher();
 
-    it('пропускает записи без числового value или без даты', async () => {
-      mockGetLogisticHistory.mockResolvedValue({
-        data: [
-          createHistoryItem({ valueDate: '2026-05-19' }),
-          createHistoryItem({ value: 5, valueDate: '' }),
-          createHistoryItem({ value: 7, valueDate: '2026-05-20' }),
-        ],
-      });
-      const resolver = getRegisteredResolver();
+    const points = await fetchBars('SEC123');
 
-      const bars = await resolver({ ticker: 'NTBVTFC:NTBVLB:SEC123' });
-
-      expect(bars).toEqual([{ time: Date.parse('2026-05-20'), open: 7, high: 7, low: 7, close: 7, volume: 0 }]);
-    });
-
-    it('парсит дату в формате dd.mm.yyyy через fallback', async () => {
-      mockGetLogisticHistory.mockResolvedValue({
-        data: [createHistoryItem({ value: 3, valueDate: '15.03.2025' })],
-      });
-      const resolver = getRegisteredResolver();
-
-      const bars = await resolver({ ticker: 'NTBVTFC:NTBVLB:SEC123' });
-
-      expect(bars[0].time).toBe(Date.UTC(2025, 2, 15));
-    });
-
-    it('возвращает [] и не ходит в API, если securityId в тикере нет', async () => {
-      const resolver = getRegisteredResolver();
-
-      const bars = await resolver({ ticker: 'NTBVTFC:NTBVLB' });
-
-      expect(bars).toEqual([]);
-      expect(mockGetLogisticHistory).not.toHaveBeenCalled();
-    });
-
-    it('возвращает [] при undefined ticker', async () => {
-      const resolver = getRegisteredResolver();
-
-      const bars = await resolver({ ticker: undefined });
-
-      expect(bars).toEqual([]);
-      expect(mockGetLogisticHistory).not.toHaveBeenCalled();
-    });
-
-    it('возвращает [] при пустой/отсутствующей истории', async () => {
-      mockGetLogisticHistory.mockResolvedValue({ data: null });
-      const resolver = getRegisteredResolver();
+    expect(mockGetLogisticHistory).toHaveBeenCalledWith({ securityId: 'SEC123' });
+    expect(points).toEqual([{ date: '2026-05-19', value: 40 }]);
+  });
 
-      const bars = await resolver({ ticker: 'NTBVTFC:NTBVLB:SEC123' });
+  it('возвращает [] при отсутствующей истории', async () => {
+    mockGetLogisticHistory.mockResolvedValue({ data: null });
+    const fetchBars = getFetcher();
 
-      expect(bars).toEqual([]);
-    });
+    expect(await fetchBars('SEC123')).toEqual([]);
   });
 });
diff --git a/src/widgets/ntb/Logistic/utils/logisticChartDataSource.ts b/src/widgets/ntb/Logistic/utils/logisticChartDataSource.ts
index cc67f1cfaca04bb6b4bb9eb15dfa7481e6a4c992..caef9cc05b5661a130db13b2b4a89fdadc9f016b 100644
--- a/src/widgets/ntb/Logistic/utils/logisticChartDataSource.ts
+++ b/src/widgets/ntb/Logistic/utils/logisticChartDataSource.ts
@@ -1,45 +1,12 @@
 import { ntbLogisticController } from '@api/controllers/ntbController';
-import { registerBarsResolver } from '@widgets/Chart/requestBars';
+import { registerNtbBarsResolver } from '@modules/ntb/utils/registerNtbBarsResolver';
 
 import { LOGISTIC_CHART_BOARD } from '../constants';
 
-import type { Logistic } from '@modules/ntb/types';
-import type { Candle } from 'moex-chart';
-
-const parseValueDate = (valueDate: string): number => {
-  const parsed = Date.parse(valueDate);
-  if (!Number.isNaN(parsed)) {
-    return parsed;
-  }
-
-  const [day, month, fullYear] = valueDate.split('.');
-  return Date.UTC(Number(fullYear), Number(month) - 1, Number(day));
-};
-
-const mapHistoryToBars = (history: Logistic[]): Candle[] => {
-  const valueByTime = new Map<number, number>();
-
-  history.forEach((item) => {
-    if (typeof item.value === 'number' && item.valueDate) {
-      valueByTime.set(parseValueDate(item.valueDate), item.value);
-    }
-  });
-
-  return Array.from(valueByTime.entries())
-    .sort(([timeA], [timeB]) => timeA - timeB)
-    .map(([time, value]) => ({ time, open: value, high: value, low: value, close: value, volume: 0 }));
-};
-
 export const registerLogisticChartSource = () => {
-  registerBarsResolver(LOGISTIC_CHART_BOARD, async ({ ticker }) => {
-    const securityId = ticker?.split(/[:.]/)[2];
-
-    if (!securityId) {
-      return [];
-    }
-
+  registerNtbBarsResolver(LOGISTIC_CHART_BOARD, async (securityId) => {
     const { data } = await ntbLogisticController.getLogisticHistory({ securityId });
 
-    return mapHistoryToBars(data ?? []);
+    return (data ?? []).map((item) => ({ date: item.valueDate, value: item.value }));
   });
 };