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


import { parseTimeframe } from '@mx-charts/charts';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';

import { moexChartTimeConverter, moexChartToIssTimeframe } from '@utils/chartToReqTimeConverter';

import { requestBars, requestRealtimeBars } from '../../requestBars';
import { ChartIndicativeData } from '../../types';

import type { Candle, Timeframes } from '@mx-charts/charts';

dayjs.extend(duration);

interface RealtimeState {
  previousTime: number | null;
  candles: Candle[];
  sessionStart: number | null;
}

function getRequestSymbol(symbolRaw?: string): string | undefined {
  const symbol = String(symbolRaw ?? '').trim();

  if (!symbol) {
    return undefined;
  }

  return symbol;
}

function getTimeframeSeconds(timeframe: Timeframes): number {
  const { candleWidth, dayjsUnit } = parseTimeframe(timeframe);

  return dayjs.duration(candleWidth, dayjsUnit).asSeconds();
}

function isValidCandle({ time, open, high, low, close, volume }: Candle): boolean {
  const valuesAreValid = [time, open, high, low, close].every(Number.isFinite);

  return (
    valuesAreValid &&
    (volume === undefined || Number.isFinite(volume)) &&
    high >= Math.max(open, close) &&
    low <= Math.min(open, close)
  );
}

function isSameCandle(first: Candle | undefined, second: Candle): boolean {
  return (
    first?.time === second.time &&
    first.open === second.open &&
    first.high === second.high &&
    first.low === second.low &&
    first.close === second.close &&
    first.volume === second.volume
  );
}

function aggregateCandles(candles: Candle[], time: number): Candle | undefined {
  const firstCandle = candles[0];
  const lastCandle = candles[candles.length - 1];

  if (!firstCandle || !lastCandle) {
    return undefined;
  }

  const high = Math.max(...candles.map((candle) => candle.high));
  const low = Math.min(...candles.map((candle) => candle.low));
  const volume = candles.reduce((total, candle) => total + (candle.volume ?? 0), 0);

  return {
    time,
    open: firstCandle.open,
    high,
    low,
    close: lastCandle.close,
    volume,
  };
}

function appendAggregatedCandle(result: Candle[], candles: Candle[], time: number): void {
  const candle = aggregateCandles(candles, time);

  if (candle) {
    result.push(candle);
  }
}

function timeframeConvolution(
  data: Candle[],
  requestedTimeframe: Timeframes,
): {
  candles: Candle[];
  state: RealtimeState;
} {
  const timeframeSeconds = getTimeframeSeconds(requestedTimeframe);
  const sortedData = [...data].sort((first, second) => first.time - second.time);
  const firstCandle = sortedData[0];

  if (!firstCandle) {
    return {
      candles: [],
      state: {
        previousTime: null,
        candles: [],
        sessionStart: null,
      },
    };
  }

  const result: Candle[] = [];

  let sessionStart = firstCandle.time;
  let bucketStart = sessionStart;
  let previousTime: number | null = null;
  let candleGroup: Candle[] = [];

  sortedData.forEach((candle) => {
    const isNewSession = previousTime !== null && candle.time - previousTime > timeframeSeconds;

    if (isNewSession) {
      appendAggregatedCandle(result, candleGroup, bucketStart);

      sessionStart = candle.time;
      bucketStart = candle.time;
      candleGroup = [candle];
      previousTime = candle.time;

      return;
    }

    const currentBucketStart =
      sessionStart + Math.floor((candle.time - sessionStart) / timeframeSeconds) * timeframeSeconds;

    if (currentBucketStart !== bucketStart) {
      appendAggregatedCandle(result, candleGroup, bucketStart);

      bucketStart = currentBucketStart;
      candleGroup = [];
    }

    candleGroup.push(candle);
    previousTime = candle.time;
  });

  appendAggregatedCandle(result, candleGroup, bucketStart);

  return {
    candles: result,
    state: {
      previousTime,
      candles: candleGroup,
      sessionStart,
    },
  };
}

function realtimeConvolution(
  timeframe: Timeframes,
  data: Candle,
  currentState: RealtimeState,
): {
  candle: Candle | undefined;
  state: RealtimeState;
} {
  const timeframeSeconds = getTimeframeSeconds(timeframe);
  const { previousTime, sessionStart } = currentState;

  if (previousTime === null || sessionStart === null || data.time - previousTime > timeframeSeconds) {
    const state: RealtimeState = {
      previousTime: data.time,
      candles: [data],
      sessionStart: data.time,
    };

    return {
      candle: aggregateCandles(state.candles, data.time),
      state,
    };
  }

  const previousBucketStart =
    sessionStart + Math.floor((previousTime - sessionStart) / timeframeSeconds) * timeframeSeconds;

  const currentBucketStart =
    sessionStart + Math.floor((data.time - sessionStart) / timeframeSeconds) * timeframeSeconds;

  let candles: Candle[];

  if (currentBucketStart !== previousBucketStart) {
    candles = [data];
  } else {
    const candleIndex = currentState.candles.findIndex((candle) => candle.time === data.time);

    if (candleIndex === -1) {
      candles = [...currentState.candles, data];
    } else {
      candles = [...currentState.candles];
      candles[candleIndex] = data;
    }
  }

  const state: RealtimeState = {
    previousTime: data.time,
    candles,
    sessionStart,
  };

  return {
    candle: aggregateCandles(candles, currentBucketStart),
    state,
  };
}

// По хорошему - класс должен быть синглтоном, чтобы кормить MoexChart одинаковой датой,
// и не плодить несколько подключений на одни символа
class DataSourceProvider {
  private readonly realtimeStates = new Map<string, RealtimeState>();

  private realtimeTimer: ReturnType<typeof setInterval> | null = null;

  public getDataSource =
    (indicativeData?: ChartIndicativeData, cb?: (timeframe: Timeframes) => void) =>
    async (timeframe: Timeframes, symbolId: string, until?: Candle): Promise<Candle[] | null> => {
      const symbol = getRequestSymbol(symbolId);

      if (!symbol) {
        return null;
      }

      cb?.(timeframe);

      const interval = moexChartTimeConverter(timeframe);
      const date = until?.time ?? Math.round(Date.now() / 1000);

      const data = await requestBars({
        currencyPair: symbol.replaceAll(':', '.'),
        interval,
        periodParams: {
          firstDataRequest: true,
          to: date,
          from: Math.round(Date.now() / 1000),
          countBack: 2000,
        },
        ticker: symbol,
        indicativeData,
      });

      const validData = data.filter(isValidCandle);

      if (validData.length === 0) {
        return null;
      }

      if (moexChartToIssTimeframe(timeframe) === timeframe) {
        if (!until) {
          const lastCandle = validData.at(-1);

          this.realtimeStates.set(symbol, {
            previousTime: lastCandle?.time ?? null,
            candles: lastCandle ? [lastCandle] : [],
            sessionStart: null,
          });
        }

        return validData;
      }

      const { candles, state } = timeframeConvolution(validData, timeframe);

      if (!until) {
        this.realtimeStates.set(symbol, state);
      }

      return candles;
    };

  public startRealtime({
    getSymbols,
    getTimeframe,
    update,
    periodMs = 5000,
    indicativeData,
  }: {
    getSymbols: () => string[];
    getTimeframe: () => Timeframes;
    update: (symbolId: string, candle: Candle) => void;
    periodMs?: number;
    indicativeData?: ChartIndicativeData;
  }): () => void {
    if (this.realtimeTimer) {
      clearInterval(this.realtimeTimer);
    }

    this.realtimeTimer = setInterval(() => {
      const timeframe = getTimeframe();

      Promise.allSettled(
        getSymbols().map((symbolId) => this.updateRealtimeSymbol(symbolId, timeframe, update, indicativeData)),
      );
    }, periodMs);

    return () => {
      if (this.realtimeTimer) {
        clearInterval(this.realtimeTimer);
      }

      this.realtimeTimer = null;
    };
  }

  private async updateRealtimeSymbol(
    symbolId: string,
    timeframe: Timeframes,
    update: (symbolId: string, candle: Candle) => void,
    indicativeData?: ChartIndicativeData,
  ): Promise<void> {
    const symbol = getRequestSymbol(symbolId);

    if (!symbol) {
      return;
    }

    const data = await requestRealtimeBars({
      currencyPair: symbol.replaceAll(':', '.'),
      interval: moexChartTimeConverter(timeframe),
      ticker: symbol,
      indicativeData,
    });

    if (!data || !isValidCandle(data)) {
      return;
    }

    const currentState = this.realtimeStates.get(symbol) ?? {
      previousTime: null,
      candles: [],
      sessionStart: null,
    };

    const currentCandle = currentState.candles.find((candle) => candle.time === data.time);

    if (isSameCandle(currentCandle, data)) {
      return;
    }

    if (moexChartToIssTimeframe(timeframe) === timeframe) {
      this.realtimeStates.set(symbol, {
        previousTime: data.time,
        candles: [data],
        sessionStart: null,
      });

      update(symbol, data);

      return;
    }

    const { candle, state } = realtimeConvolution(timeframe, data, currentState);

    this.realtimeStates.set(symbol, state);

    if (candle) {
      update(symbol, candle);
    }
  }
}

export { DataSourceProvider };

















import React from 'react';

import { SymbolSearchModal } from '@widgets/Chart/components/MoexChart/components/SymbolSearchModal';

import { CompareModal } from './components/CompareModal';
import { useMoexChart } from './hooks';

import type { ChartIndicativeData, SelectedInstrument } from '../../types';

import type { SymbolInfoInput } from '@mx-charts/charts';

import '@mx-charts/charts/dist/styles.css';

interface TRProps {
  symbolInfo: SymbolInfoInput;
  indicativeData?: ChartIndicativeData;
  widgetId: number;
  addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
}

function MoexChart({ symbolInfo, indicativeData, widgetId, addInstrumentFromModal }: TRProps) {
  const {
    containerRef,
    isCompareOpen,
    isSymbolSearchOpen,
    compareManagerRef,
    setIsCompareOpen,
    setIsSymbolSearchOpen,
  } = useMoexChart({
    symbolInfo,
    indicativeData,
  });

  const handleSymbolChange = (instrument: SelectedInstrument): void => {
    addInstrumentFromModal([instrument]);
  };

  return (
    <div
      style={{
        flex: '1 1 0',
        minHeight: 0,
        minWidth: 0,
      }}
    >
      <div ref={containerRef} />

      {isCompareOpen && (
        <CompareModal
          compareManager={compareManagerRef}
          widgetId={widgetId}
          isOpen={isCompareOpen}
          setOpen={setIsCompareOpen}
        />
      )}

      {isSymbolSearchOpen && (
        <SymbolSearchModal
          widgetId={widgetId}
          isOpen={isSymbolSearchOpen}
          setOpen={setIsSymbolSearchOpen}
          onSymbolChange={handleSymbolChange}
        />
      )}
    </div>
  );
}

export default React.memo(MoexChart);
















import { 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 { MOEX_CHART_CONFIG } from '../constants';
import { DataSourceProvider } from '../dataSourceProvide';

import type { __CompareManager__, ChartSeriesType, IMoexChart, SymbolInfoInput } from '@mx-charts/charts';

interface TUseMoexChartProps {
  symbolInfo: SymbolInfoInput;
  indicativeData?: ChartIndicativeData;
}

interface SnapshotSettings {
  timeframe: Timeframes;
  interval?: Intervals;
  chartSeriesType?: ChartSeriesType;
}

export const useMoexChart = ({ symbolInfo, indicativeData }: TUseMoexChartProps) => {
  const moexChartState = useSelectProperties(
    (widgetProperties: Partial<WidgetProperties>) => widgetProperties.moexChartState,
  );

  const { updateProperties } = useChangeProperties<WidgetProperties>();

  const [isCompareOpen, setIsCompareOpen] = useState(false);
  const [isSymbolSearchOpen, setIsSymbolSearchOpen] = useState(false);

  const containerRef = useRef<HTMLDivElement | null>(null);
  const chartRef = useRef<MoexChart | null>(null);
  const compareManagerRef = useRef<__CompareManager__ | null>(null);

  const currentSymbolInfoRef = useRef<SymbolInfoInput>(symbolInfo);
  const timeframeRef = useRef<Timeframes | undefined>(moexChartState?.timeframe);
  const savedDataRef = useRef<string | undefined>(moexChartState?.savedData);
  const initialIntervalRef = useRef<Intervals | undefined>(moexChartState?.initialInterval);
  const initialChartSeriesTypeRef = useRef<ChartSeriesType | undefined>(moexChartState?.initialChartSeriesType);
  const updateTimeframeRef = useRef<((timeframe: Timeframes) => void) | null>(null);

  useEffect(() => {
    const currentSymbolInfo = currentSymbolInfoRef.current;

    if (
      currentSymbolInfo.symbolId === symbolInfo.symbolId &&
      currentSymbolInfo.symbol === symbolInfo.symbol &&
      currentSymbolInfo.symbolName === symbolInfo.symbolName
    ) {
      return;
    }

    currentSymbolInfoRef.current = symbolInfo;
    chartRef.current?.setSymbol(symbolInfo);
  }, [symbolInfo]);

  useEffect(() => {
    timeframeRef.current = moexChartState?.timeframe;
    savedDataRef.current = moexChartState?.savedData;
  }, [moexChartState?.savedData, moexChartState?.timeframe]);

  updateTimeframeRef.current = (timeframe: Timeframes): void => {
    if (timeframeRef.current === timeframe) {
      return;
    }

    timeframeRef.current = timeframe;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        timeframe,
      };
    });
  };

  const saveSnapshot = useCallback((): void => {
    const snapshot = chartRef.current?.getSnapshot();

    if (!snapshot) {
      return;
    }

    const savedData = JSON.stringify(snapshot);

    if (savedDataRef.current === savedData) {
      return;
    }

    savedDataRef.current = savedData;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        timeframe: timeframeRef.current ?? Timeframes['1m'],
        savedData,
      };
    });
  }, [updateProperties]);

  const getSnapshotWithCurrentSettings = (
    snapshot: typeof MOEX_CHART_CONFIG.snapshot | IMoexChart['snapshot'],
    { timeframe, interval, chartSeriesType }: SnapshotSettings,
  ): IMoexChart['snapshot'] => ({
    ...snapshot,
    charts: snapshot.charts.map((chartSnapshot) => ({
      ...chartSnapshot,
      ...currentSymbolInfoRef.current,
      timeframe,
      ...(interval ? { interval } : {}),
      ...(chartSeriesType ? { chartSeriesType } : {}),
    })),
  });

  const applySnapshot = (): void => {
    if (!savedDataRef.current || !chartRef.current) {
      return;
    }

    const savedSnapshot = JSON.parse(savedDataRef.current) as IMoexChart['snapshot'];
    const timeframe = timeframeRef.current ?? Timeframes['1m'];

    chartRef.current.setSnapshot(getSnapshotWithCurrentSettings(savedSnapshot, { timeframe }));
    compareManagerRef.current = chartRef.current.getCompareManager();
  };

  useEffect(() => {
    const container = containerRef.current;

    if (!container) {
      return undefined;
    }

    const timeframe = timeframeRef.current ?? Timeframes['1m'];
    const initialInterval = initialIntervalRef.current;
    const initialChartSeriesType = initialChartSeriesTypeRef.current;

    const savedSnapshot = savedDataRef.current
      ? (JSON.parse(savedDataRef.current) as IMoexChart['snapshot'])
      : MOEX_CHART_CONFIG.snapshot;

    const dataProvider = new DataSourceProvider();

    const chart = new MoexChart({
      ...MOEX_CHART_CONFIG,
      container,
      snapshot: getSnapshotWithCurrentSettings(savedSnapshot, {
        timeframe,
        interval: initialInterval,
        chartSeriesType: initialChartSeriesType,
      }),
      chartCollectionPreset: {
        ...MOEX_CHART_CONFIG.chartCollectionPreset,
        openCompareModal: () => setIsCompareOpen(true),
        openSymbolSearchModal: () => setIsSymbolSearchOpen(true),
        getDataSource: dataProvider.getDataSource(indicativeData, (nextTimeframe) => {
          updateTimeframeRef.current?.(nextTimeframe);
        }),
        startRealtime: (getSymbols, getTimeframe, update) =>
          dataProvider.startRealtime({
            getSymbols,
            getTimeframe,
            update,
            indicativeData,
          }),
      },
    });

    chartRef.current = chart;
    compareManagerRef.current = chart.getCompareManager();

    if (initialInterval || initialChartSeriesType) {
      initialIntervalRef.current = undefined;
      initialChartSeriesTypeRef.current = undefined;

      updateProperties((state) => {
        state.moexChartState = {
          ...state.moexChartState,
          initialInterval: undefined,
          initialChartSeriesType: undefined,
        };
      });
    }

    const intervalId = setInterval(() => {
      saveSnapshot();
    }, 1000);

    return () => {
      clearInterval(intervalId);
      saveSnapshot();

      chartRef.current = null;
      compareManagerRef.current = null;
      chart.destroy();
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps -- график пересоздаётся только при изменении источника индикативных данных
  }, [indicativeData?.key]);

  return {
    containerRef,
    isCompareOpen,
    isSymbolSearchOpen,
    compareManagerRef,
    setIsCompareOpen,
    setIsSymbolSearchOpen,
    saveSnapshot,
    applySnapshot,
    hasSavedSnapshot: Boolean(moexChartState?.savedData),
  };
};

















import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';

import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';

import { getStore } from '@store/store';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';

import { DEFAULT_SYMBOL } from '../const';

import { useChartPublicContext } from './useChartPublicContext';

import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps, SelectedInstrument } from '../types';

import type { SymbolInfoInput } from '@mx-charts/charts';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import type { Widget } from 'types/Widgets';

interface UseChartComponentFacadeReturn {
  dropDownOpen: boolean;
  setDropdownOpen: Dispatch<SetStateAction<boolean>>;
  currentSymbolInfo: SymbolInfoInput;
  isWidgetHeaderContextMenuOpen: boolean;
  setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
  onDropInstrument: (symbolId: string, withUpdate?: boolean) => void;
  addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
  isOver: boolean;
}

type SaveContentPropsOptions = Partial<WidgetProperties['chartState']> & {
  withUpdate?: boolean;
  cleanIndicativeData?: boolean;
};

function useInstrumentChange({
  saveContentProps, 
  setIsWidgetHeaderContextMenuOpen, 
  currentSymbolInfoRef, 
  setCurrentSymbolInfo, 
  triggerRelatedWidgetsToUpdateRef, 
  widgetId
}: {
  saveContentProps: (value: SaveContentPropsOptions) => void, 
  setIsWidgetHeaderContextMenuOpen: (value: boolean) => void, 
  currentSymbolInfoRef: MutableRefObject<SymbolInfoInput>, 
  setCurrentSymbolInfo: (value: SymbolInfoInput) => void, 
  triggerRelatedWidgetsToUpdateRef: MutableRefObject<(key: string) => void>, 
  widgetId: number
}) {
  const dispatch = useDispatch();
  const currentSymbolInfoRefCrunch = currentSymbolInfoRef

  const onInstrumentChange = useCallback(
    (symbolInfo: SymbolInfoInput, withUpdate?: boolean, unbind = true): void => {
      const symbolId = symbolInfo.symbolId.trim();

      if (!symbolId) {
        return;
      }

      setIsWidgetHeaderContextMenuOpen(false);

      const currentValue = currentSymbolInfoRefCrunch.current;
      const symbolIdChanged = currentValue.symbolId !== symbolId;

      const symbol = symbolInfo.symbol?.trim() || (symbolIdChanged ? undefined : currentValue.symbol);
      const symbolName = symbolInfo.symbolName?.trim() || (symbolIdChanged ? undefined : currentValue.symbolName);

      const nextSymbolInfo: SymbolInfoInput = {
        symbolId,
        symbol,
        symbolName,
      };

      const symbolChanged = currentValue.symbol !== symbol;
      const symbolNameChanged = currentValue.symbolName !== symbolName;

      if (!symbolIdChanged && !symbolChanged && !symbolNameChanged) {
        return;
      }

      currentSymbolInfoRefCrunch.current = nextSymbolInfo;
      setCurrentSymbolInfo(nextSymbolInfo);

      if (symbolIdChanged && unbind) {
        dispatch(unbindWidgets({ widgetId }));
      }

      // при смене инструмента очищаем индикативные данные виджета график
      // т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
      saveContentProps({
        savedInstrument: symbolId,
        savedSymbol: symbol,
        savedSymbolName: symbolName,
        withUpdate,
        cleanIndicativeData: symbolIdChanged,
      });

        if (symbolIdChanged) {
          triggerRelatedWidgetsToUpdateRef.current(symbolId);
        }
      },
      [dispatch, saveContentProps, widgetId, currentSymbolInfoRefCrunch, setCurrentSymbolInfo, setIsWidgetHeaderContextMenuOpen, triggerRelatedWidgetsToUpdateRef ],
    );

   const addInstrumentFromModal = useCallback(
    (instruments: SelectedInstrument[]): void => {
      const instrument = instruments[0];

      if (!instrument?.issKey) {
        return;
      }

      onInstrumentChange({
        symbolId: instrument.issKey,
        symbol: instrument.symbol,
        symbolName: instrument.displayName,
      });
    },
    [onInstrumentChange],
  );

  const onInstrumentChangeFromBind = useCallback(
    (symbolInfo: SymbolInfoInput): void => {
      onInstrumentChange(symbolInfo, true, false);
    },
    [onInstrumentChange],
  );

  const onDropInstrument = useCallback(
    (symbolId: string, withUpdate?: boolean): void => {
      onInstrumentChange({ symbolId }, withUpdate);
    },
    [onInstrumentChange],
  );

  return {addInstrumentFromModal, onInstrumentChangeFromBind, onDropInstrument, onInstrumentChange}
}

export default function useChartComponentFacade(props: ChartContainerProps): UseChartComponentFacadeReturn {
  const { widgetId } = props;

  const dispatch = useDispatch();

  const [isOver, setIsOver] = useState(false);
  const [dropDownOpen, setDropdownOpen] = useState(false);
  const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);

  const widgetProperties = useAppSelect(
    (state) => state.widgets.widgets.find(({ id }) => id === widgetId)?.widgetContentProps,
  ) as WidgetProperties | undefined;

  // TODO временно реф из-за непредсказуемого изменения если используется useState,
  // нужен рефакторинг и вернуть обратно useState
  const currentSymbolInfoRef = useRef<SymbolInfoInput>(DEFAULT_SYMBOL)

  const savedInstrument = widgetProperties?.chartState?.savedInstrument

  if(savedInstrument){
    currentSymbolInfoRef.current = {
      symbolId: savedInstrument,
      symbol: widgetProperties?.chartState?.savedSymbol,
      symbolName: widgetProperties?.chartState?.savedSymbolName,
    };
  }

  const [currentSymbolInfo, setCurrentSymbolInfo] = useState<SymbolInfoInput>(currentSymbolInfoRef.current);

  const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
    widgetId,
  });

  const triggerRelatedWidgetsToUpdateRef = useRef(triggerRelatedWidgetsToUpdate);

  useEffect(() => {
    triggerRelatedWidgetsToUpdateRef.current = triggerRelatedWidgetsToUpdate;
  }, [triggerRelatedWidgetsToUpdate]);

  const saveContentProps = useCallback(
    (value: SaveContentPropsOptions): void => {
      const { withUpdate, cleanIndicativeData, ...chartStateValue } = value;

      const oldProps = (getStore().getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget | undefined)
        ?.widgetContentProps;

      if (!oldProps) {
        return;
      }

      dispatch(
        addContentPropsToWidget({
          id: widgetId,
          withoutSend: !withUpdate,
          widgetContentProps: {
            chartState: {
              ...oldProps.chartState,
              ...chartStateValue,
            },
            // при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
            indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
          },
        }),
      );
    },
    [dispatch, widgetId],
  );

  const { addInstrumentFromModal, onInstrumentChangeFromBind, onDropInstrument, onInstrumentChange } = useInstrumentChange({
    saveContentProps, 
    setIsWidgetHeaderContextMenuOpen, 
    currentSymbolInfoRef, 
    setCurrentSymbolInfo,
    triggerRelatedWidgetsToUpdateRef, 
    widgetId
  })

  useChartPublicContext({
    setCurrInstrument: onInstrumentChangeFromBind,
    widgetId,
    getMasterInstrumentFromPublicContext,
  });

  useEffect(() => {
    triggerRelatedWidgetsToUpdateRef.current(currentSymbolInfoRef.current.symbolId);
  }, []);

  useEffect(() => {
    const unsubscribe = communicator.listen(
      {
        messageType: CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT,
      },
      (message) => {
        const symbolId = (message as Record<number, string>)[widgetId];

        if (symbolId) {
          onInstrumentChange({ symbolId });
        }
      },
    );

    const unsubscribeHighlighter = communicator.listen(
      {
        messageType: HIGHLIGHT_WIDGET_EVENT,
      },
      (message) => {
        const typedMessage = message as Record<number, boolean>;

        if (Object.keys(typedMessage).includes(String(widgetId))) {
          setIsOver(typedMessage[widgetId]);
        }
      },
    );

    return () => {
      unsubscribe();
      unsubscribeHighlighter();
    };
  }, [onInstrumentChange, widgetId]);

  return {
    dropDownOpen,
    setDropdownOpen,
    currentSymbolInfo,
    isWidgetHeaderContextMenuOpen,
    setIsWidgetHeaderContextMenuOpen,
    onDropInstrument,
    addInstrumentFromModal,
    isOver,
  };
}

















import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

import { indicativeQuotesController } from '@api/controllers/indicativeQuotesController';
import api from '@api/index';

import { candleToBar } from '@utils/candleToBar';

import { ChartIndicativeData } from './types';
import { isIndicativeTicker } from './utils/isIndicativeTicker';
import { transformKeyToLowerCase } from './utils/transformKeyToLowerCase';

import type { Candle } from '@mx-charts/charts';

dayjs.extend(utc);

export interface PeriodParams {
  from: number;
  to: number;
  countBack: number;
  firstDataRequest: boolean;
}
export interface HistoryMetadata {
  noData: boolean;
}
export type HistoryCallback = (candles: Candle[], metadata: HistoryMetadata) => void;
export type SubscribeBarsCallback = (candle: Candle) => void;

interface RequestBarsArgs {
  currencyPair: string;
  interval: string;
  periodParams: PeriodParams;
  onHistoryCallback?: HistoryCallback;
  ticker?: string;
  indicativeData?: ChartIndicativeData;
}

interface RequestRealtimeBarsArgs {
  currencyPair: string;
  interval: string;
  ticker?: string;
  indicativeData?: ChartIndicativeData;
  onRealtimeCallback?: SubscribeBarsCallback;
}

export type CustomBarsResolver = (args: {
  ticker?: string;
  currencyPair: string;
  periodParams: PeriodParams;
  interval: string;
}) => Promise<Candle[]>;

const barsResolvers = new Map<string, CustomBarsResolver>();

export const registerBarsResolver = (boardKey: string, resolver: CustomBarsResolver) => {
  barsResolvers.set(boardKey, resolver);
};

const getResolverKey = (ticker?: string) => {
  const [source, board] = ticker?.split(/[:.]/) ?? [];

  if (!source || !board) {
    return '';
  }

  return `${source}:${board}`;
};

const getBarsResolver = (ticker?: string) => barsResolvers.get(getResolverKey(ticker));

export async function requestBars({
  currencyPair,
  interval,
  periodParams,
  onHistoryCallback,
  ticker,
  indicativeData,
}: RequestBarsArgs): Promise<Candle[]> {
  const customResolver = getBarsResolver(ticker);
  if (customResolver) {
    try {
      const customBars = await customResolver({ ticker, currencyPair, periodParams, interval });
      const toMs = periodParams.to * 1000;
      const olderBars = customBars.filter((bar) => bar.time < toMs);
      onHistoryCallback?.(olderBars, { noData: olderBars.length === 0 });
      return olderBars;
    } catch (e) {
      onHistoryCallback?.([], { noData: true });
      return [];
    }
  }

  const date = new Date(periodParams.to * 1000);
  const year = date.getUTCFullYear();
  const month = `0${date.getUTCMonth() + 1}`.slice(-2);
  const day = `0${date.getUTCDate()}`.slice(-2);
  const hours = `0${date.getUTCHours()}`.slice(-2);
  const minutes = `0${date.getUTCMinutes()}`.slice(-2);
  const seconds = `0${date.getUTCSeconds()}`.slice(-2);

  const hasIndicativeBoardInTicker = isIndicativeTicker(ticker);

  // если при инициализации графика были данные indicativeData и текущий инструмент совпадает
  // то отправляем запрос на индикатив
  // иначе на инструменты
  const isIndicativeInstrument = (indicativeData && indicativeData.key === ticker) || hasIndicativeBoardInTicker;

  if (isIndicativeInstrument) {
    const dateStr = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;

    const lowerCaseKey = transformKeyToLowerCase(currencyPair);

    if (!lowerCaseKey) {
      onHistoryCallback?.([], { noData: true });
      return [];
    }

    try {
      const { data } = await indicativeQuotesController.getCandles({
        count: periodParams.countBack,
        key: lowerCaseKey,
        date: dateStr,
        interval,
      });

      const candles = data.indicativeCandles.reverse().map(candleToBar);

      onHistoryCallback?.(candles, {
        noData: candles.length === 0,
      });

      return candles;
    } catch {
      onHistoryCallback?.([], {
        noData: true,
      });

      return [];
    }
  }

  const dateStr = `${year}-${month}-${day}%20${hours}:${minutes}:${seconds}`;

  try {
    const { data } = await api.getBars({
      currencyPair,
      date: dateStr,
      interval,
      count: periodParams.countBack,
      ticker,
    });

    const bars = data.reverse().map(candleToBar);

    onHistoryCallback?.(bars, {
      noData: bars.length === 0,
    });

    return bars;
  } catch {
    onHistoryCallback?.([], {
      noData: true,
    });

    return [];
  }
}

export async function requestRealtimeBars({
  currencyPair,
  interval,
  ticker,
  onRealtimeCallback,
  indicativeData,
}: RequestRealtimeBarsArgs): Promise<Candle | undefined> {
  if (getBarsResolver(ticker)) {
    return;
  }

  const hasIndicativeBoardInTicker = isIndicativeTicker(ticker);

  // если при инициализации графика были данные indicativeData и текущий инструмент совпадает
  // то отправляем запрос на индикатив
  // иначе на инструменты
  const isIndicativeInstrument = (indicativeData && indicativeData.key === ticker) || hasIndicativeBoardInTicker;

  if (isIndicativeInstrument) {
    const lowerCaseKey = transformKeyToLowerCase(currencyPair);

    if (!lowerCaseKey) {
      return undefined;
    }

    try {
      const { data } = await indicativeQuotesController.getCandles({
        count: 1,
        key: lowerCaseKey,
        date: dayjs().utc().add(1, 'minute').format('YYYY-MM-DDTHH:mm:ss'),
        interval,
      });

      if (data.indicativeCandles.length === 0) {
        return undefined;
      }

      const sortedData = [...data.indicativeCandles].sort(
        (first, second) => new Date(first.end).valueOf() - new Date(second.end).valueOf(),
      );

      const firstCandle = sortedData[0];

      if (!firstCandle) {
        return undefined;
      }

      const bar = candleToBar(firstCandle);

      onRealtimeCallback?.(bar);

      return bar;
    } catch (error) {
      console.error('error from requestRealTimeBars indicativeQuotesController: ', error);

      return undefined;
    }
  }

  try {
    const { data } = await api.getBars({
      currencyPair,
      date: dayjs().utc().add(1, 'minute').format('YYYY-MM-DD%20HH:mm:ss'),
      interval,
      count: 1,
      ticker,
    });

    if (data.length === 0) {
      return undefined;
    }

    const sortedData = [...data].sort(
      (first, second) => new Date(first.end).valueOf() - new Date(second.end).valueOf(),
    );

    const firstCandle = sortedData[0];

    if (!firstCandle) {
      return undefined;
    }

    const bar = candleToBar(firstCandle);

    onRealtimeCallback?.(bar);

    return bar;
  } catch (error) {
    console.error('error from requestRealTimeBars: ', error);

    return undefined;
  }
}