Загрузка данных
import dayjs, { type Dayjs } from 'dayjs';
import { feedbackController } from '@api/controllers/feedbackController';
import { commonDateFormat } from '@configs/standartDateFormat';
import { AboutInstrumentTradingResultType } from 'types/AboutInstrument';
import { Candle } from 'types/Candles';
import axios, {
axiosCurves,
axiosIndicative,
axiosInstanceFormalization,
axiosInstanceNews,
axiosInstanceNTBAnalytics,
axiosInstanceNTBTrading,
axiosInstanceNTPro,
axiosInstanceSPFI,
axiosInstanceTD,
axiosInstanceTMD,
axiosInstanceTus,
uninterceptedAxiosInstance,
} from './axios';
import { abbrevationsController } from './controllers/abbrevationsController';
import { candlesController } from './controllers/candles';
import { frontExceptionController } from './controllers/frontExceptionController';
import { newsSettingsController } from './controllers/newsController';
import { noTradeChatController } from './controllers/noTradeChatController';
import { tradeChatController } from './controllers/tradeChatController';
import { widgetPropertiesController } from './controllers/widgetProperties';
import { widgetsController } from './controllers/widgets';
import { workspaceController } from './controllers/workspace';
import type { ResponseSearchSmartType } from '@components/InputSearchCommand/type/type';
import type { Contract } from '@modules/contracts';
import type { BoardType, Counterparty } from '@widgets/AboutInstrument/types';
import type { QuotesNTProTypeServer } from '@widgets/QuotesNTPro/type';
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { IChatFolder, IChatMessages, ICreateChatFolder, IUpdateChatFolder } from 'types/Chats';
import type {
CurveValue,
IndicativeCurveFilters,
IndicativeCurveType,
IssCurveFilters,
IssCurveGroup,
IssCurveType,
ZeroCouponCurveLiveRequestParams,
ZeroCouponCurveLiveResponse,
} from 'types/Curves';
import type { CustomerDataType } from 'types/Customers';
import type { Firm } from 'types/Firm';
import type { FixingData } from 'types/FixingData';
import type { Instrument } from 'types/Instruments';
import type { INewsFeed, INewsItem } from 'types/News';
// import type { NSDCostType } from 'types/NSDCost';
import type { Payment } from 'types/Payment';
import type { QuotesType } from 'types/Quotes';
import type { GetBarsParams } from 'types/Requests/BarsTypes';
import type { SittingCB } from 'types/SittingCB';
import type { User, UserSettings } from 'types/User';
const api = {
getBars({ currencyPair, interval, date, count }: GetBarsParams) {
return axiosInstanceTD.get<Candle[]>(
`/api/v1/candles/list/offset?secId=${currencyPair}&date=${date}&interval=${interval}&count=${count}`,
);
},
getQuotes() {
return axios.post('api/v1/blotters/quotes', {
timestamp: -1,
keys: [
'AIX:MAIN:BAST',
'AIX:MAIN:Bond',
'AIX:MAIN:0000000000000',
'AIX:MAIN:aix123',
'AIX:AOTC:EQOTCa',
'AIX:MAIN:TickEQ1',
'AIX:MAIN:COVID19',
'AIX:MAIN:HSBK',
'AIX:MAIN:Forte_AIX',
'AIX:MAIN:Tatn_AIX',
'AIX:MAIN:170920201635',
'INT:IUSA:123987',
'INT:IUSA:HSBK_priv_IUSA',
'INT:IUSA:AAPL',
'INT:IUSA:aaa8888811122',
'INT:IUSA:aaa1',
'INT:IUSA:171107122020',
'KSE:TQBR:RAHT',
'KSE:EQBR:HSBKp',
'KSE:TQBR:KZTK',
'KSE:TQBR:KZAP',
'KSE:TQBR:HSBK',
'KSE:TQBR:hydro10',
'INT:ILSE:KAZ',
'INT:IOTC:EQOTCI',
'INT:ILSE:tick_test1201',
'INT:IOTC:test_IOTS1201',
'INT:ILSE:tick_009',
'INT:ILSE:13360812',
'INT:ILSE:Forte_LSE',
'INT:IUSA:Test666',
],
});
},
getMarketDepth(key: Contract['key']) {
return axiosInstanceTMD.post('/api/v1/blotters/marketdepths', {
key,
timestamp: -1,
});
},
workspaceController,
widgetsController,
widgetPropertiesController,
frontExceptionController,
candlesController,
newsSettingsController,
getChatCustomers(market?: string, spfiTradingPermission?: boolean) {
return axios.get<CustomerDataType[]>('/api/v1/data/cache/customers', { params: { market, spfiTradingPermission } });
},
getCounterparties() {
return axios.get<CustomerDataType[]>('/api/v1/data/customers');
},
getCounterpartiesById(id: number) {
return axios.get<Counterparty>(`/api/v1/data/customers/${id}`, {
full: true,
} as AxiosRequestConfig<{ full: boolean }>);
},
getInstrumentById(id: number): Promise<AxiosResponse<Instrument>> {
return axios.get(`/api/v1/data/instruments/${id}`);
},
getInstrumentList(): Promise<AxiosResponse<Instrument[]>> {
return axios.get('/api/v1/data/instruments?');
},
tradeChatController,
noTradeChatController,
getNewsList(dateRange: string[]) {
return axiosInstanceTD.get(`news/getList?from=${dateRange[0]}&till=${dateRange[1]}`);
},
getQuotesList() {
return axiosInstanceTD.get('quotes/');
},
getBoardByInstrumentId(id: number, floorCode: string) {
return axios.get<BoardType[]>(`/api/v1/data/boards?floorCode=${floorCode}&instrumentId=${id}`);
},
getDealsList(isTradeChat = false) {
return axios.get(`/api/v1/blotters/deals${isTradeChat ? '?isTradeChat=true' : ''}`);
},
hideTrade(talkId: number) {
return axios.put(`api/v1/blotters/deals/hide/${talkId}`);
},
getTermsDict() {
return axiosInstanceTD.get<{ dictionary: Record<string, string> }>('/api/v1/contracts/dictionary/term');
},
getBoardsDict() {
return axiosInstanceTD.get<{ dictionary: Record<string, string> }>('/api/v1/contracts/dictionary/board');
},
getInstrGroupDict() {
return axiosInstanceTD.get<{ dictionary: Record<string, string> }>('/api/v1/contracts/dictionary/instrGroupType');
},
getIssContracts() {
return axiosInstanceTD.get<Contract[]>('/api/v1/contracts');
},
getIndicativeContracts() {
return axiosIndicative.get<Contract[]>('/api/v1/contracts');
},
getNtProContracts() {
return axiosInstanceNTPro.get<Contract[]>('/api/v1/contracts');
},
getSapfirContracts() {
return axiosInstanceSPFI.get<Contract[]>('/api/v1/contracts');
},
getNtbAnalyticsContracts() {
return axiosInstanceNTBAnalytics.get<Contract[]>('/api/v1/contracts');
},
getNtbContracts() {
return axiosInstanceNTBTrading.get<Contract[]>('/api/v1/contracts');
},
getFixingsList(): Promise<AxiosResponse<FixingData[]>> {
return axiosInstanceTD.get('/api/fixings/');
},
getFixingById(key: string): Promise<AxiosResponse<FixingData[]>> {
return axiosInstanceTD.get(`/api/fixings?code=${key}`);
},
getQuotesByKeys(keys: string[], signal?: AbortSignal) {
return axiosInstanceTD.put<QuotesType[]>('/api/v1/quotes', keys, { signal });
},
getQuotesByIndicativeKeys(keys: string[], signal?: AbortSignal) {
return axiosIndicative.put<QuotesType[]>('/api/v1/quotes', keys, { signal });
},
getQuotesListWithParams({ skip, limit }: { skip: number; limit: number }) {
return axiosInstanceTD.get<QuotesType[]>('/api/v1/quotes/list', {
params: {
skip,
limit,
},
});
},
getTurnoversByDate(date: Date) {
const year = date.getFullYear();
const month = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1;
const day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
const newDate = `${year}-${month}-${day}`;
return axiosInstanceTD.get(`/api/v1/turnovers?date=${newDate}`);
},
/** Кривые */
getCurveGroups() {
return axiosCurves.get<IssCurveGroup[]>('/api/v1/data/curves/groups');
},
getCurves() {
return axiosCurves.get<IssCurveType[]>('/api/v1/data/curves');
},
getCurvesFilter: (params: { groupCurveId?: number; id?: number; currencyCode?: string } = {}) =>
axiosCurves.get<IssCurveFilters>('/api/v1/data/curvesfilter', { params }),
getCurvePoints(groupId: number, curveId: number, date: Date | string) {
const newDate = date instanceof Date ? dayjs(date).format(commonDateFormat.backendDateFormat) : date;
return axiosCurves.get<CurveValue[]>(`/api/v1/data/curvepoints/${groupId}/${curveId}/${newDate}`);
},
getZeroCouponCurveLive: (params?: ZeroCouponCurveLiveRequestParams) =>
axiosInstanceTD.post<ZeroCouponCurveLiveResponse>('/api/v1/kbd/live', params),
/** Кривые индикативные котировок */
getIndicativeCurves: () => axiosIndicative.get<IndicativeCurveType[]>('/api/v1/data/curves'),
getIndicativeCurveFilter: (params: { groupCurveId?: number; curveId?: string; currencyCode?: string } = {}) =>
axiosIndicative.get<IndicativeCurveFilters>('/api/v1/data/curvesfilter', { params }),
getIndicativeCurvePoints(key: string, date: Dayjs) {
const formattedDate = date.format(commonDateFormat.backendDateFormat);
return axiosIndicative.get<CurveValue[]>(`/api/v1/data/curvepoints/${key}/${formattedDate}`);
},
getQuoteByIssKey(key: string) {
const issKeyParts = key.split(':');
return axiosInstanceTD.get<QuotesType>(
`/api/v1/quotes?secId=${issKeyParts[2]}&boardId=${issKeyParts[1]}&tradingSystem=${issKeyParts[0]}`,
);
},
getPaymentsByIsin(isin: string) {
return axiosInstanceTD.get<Payment[]>(`/api/v1/payments?isin=${isin}`);
},
getTradingResults(security: string, date: string | Dayjs, board: string) {
return axiosInstanceTD.get<AboutInstrumentTradingResultType[]>(
`api/trading-result?mode=instrument&security=${security}&till=${date}&from=${date}&board=${board}`,
);
},
getQuotesNTPro() {
return axiosInstanceNTPro.get<QuotesNTProTypeServer[]>('/quotes');
},
getIndexesBySecId(key: string) {
const issKeyParts = key.split(':');
return axios.get(`/api/v1/data/indexes/${issKeyParts[2]}`);
},
getUserPermissions(): Promise<AxiosResponse<string[]>> {
return axios.get('/api/v1/permission/user');
},
/** Получение основной информации по пользователю */
getCurrentUserInfo(): Promise<AxiosResponse<User>> {
return axios.get('api/v1/users/current');
},
updateMenuLocked(id: number, locked: boolean): Promise<AxiosResponse<User>> {
return axios.patch('api/v1/users/settings', {
user_id: id,
is_menu_locked: locked,
});
},
updateUser(user: User): Promise<AxiosResponse<User>> {
return axios.put('api/v1/users', user);
},
getCurrentUserSettings(id: number) {
return axios.get<UserSettings>(`/api/v1/notification-users/${id}`);
},
updateCurrentUserSettings(settigs: UserSettings) {
return axios.put('api/v1/notification-users', settigs);
},
getSearchSmart(search: string, signal: AbortSignal) {
return axiosInstanceTus.get<ResponseSearchSmartType>(`/api/v1/search-smart?search=${encodeURIComponent(search)}`, {
signal,
});
},
healthcheckTd(): Promise<AxiosResponse<{ status: string }>> {
return axiosInstanceTD.get('/actuator/health-td');
},
healthcheckTb(): Promise<AxiosResponse<{ status: string }>> {
return axios.get('/actuator/health-tb');
},
healthcheckSPFI(): Promise<AxiosResponse<{ status: string }>> {
return axiosInstanceSPFI.get('/actuator/health-spfi');
},
healthcheckForm(): Promise<AxiosResponse<{ status: string }>> {
return axiosInstanceFormalization.get('/actuator/health-form');
},
connectionCheckRequest(): Promise<AxiosResponse<{ status: string }>> {
return uninterceptedAxiosInstance.get('https://iss.moex.com/iss/sitenews/');
},
getFirms(): Promise<AxiosResponse<Firm[]>> {
return axios.get('/api/v1/data/cache/firms');
},
abbrevationsController,
feedbackController,
// Отключено в рамках TRADERADAR-12666 Отключение источника НРД
// getNSDCost() {
// return axiosInstanceTD.get<NSDCostType[]>('/api/v1/data/nsdCost');
// },
getChatFolders(): Promise<AxiosResponse<IChatFolder[]>> {
return axiosInstanceFormalization.get('/api/v1/folders');
},
createChatFolder(folderData: ICreateChatFolder): Promise<AxiosResponse<IChatFolder>> {
return axiosInstanceFormalization.post('/api/v1/folders', folderData);
},
deleteChatFolder(folderId: number): Promise<AxiosResponse> {
return axiosInstanceFormalization.delete(`/api/v1/folders/${folderId}`);
},
updateChatFolder(folderData: IUpdateChatFolder): Promise<AxiosResponse<IChatFolder>> {
return axiosInstanceFormalization.patch(`/api/v1/folders/${folderData.folderId}`, folderData);
},
updateFolderPosition(folderPosition: Record<string, number>): void {
axiosInstanceFormalization.post('/api/v1/folders/positions', folderPosition);
},
sendChatMessages(data: IChatMessages): Promise<AxiosResponse> {
return axiosInstanceFormalization.post('/api/v1/chat-messages', data);
},
getNewsItemById(newsId: string): Promise<AxiosResponse<INewsItem>> {
return axiosInstanceNews.get(`/api/v1/news/${newsId}`);
},
findNewsByText(text?: string): Promise<AxiosResponse<INewsItem[]>> {
const requestUrl = `/api/v1/news/search${text ? `?search=${encodeURIComponent(text)}` : ''}`;
return axiosInstanceNews.get(requestUrl);
},
getCombinedNews(searchParams?: string): Promise<AxiosResponse<INewsItem[]>> {
const requestUrl = searchParams ? `/api/v1/news/search?${searchParams}` : '/api/v1/news/search';
return axiosInstanceNews.get(requestUrl);
},
getNewsFeeds(): Promise<AxiosResponse<INewsFeed[]>> {
return axiosInstanceNews.get('/api/v1/newsfeeds');
},
getContractsIssInfo(): Promise<AxiosResponse<{ key: string }>> {
return axiosInstanceTD.get('/api/v1/contracts/info');
},
getSittingCB() {
return axiosInstanceTD.get<SittingCB>('/api/v1/data/sittingCB');
},
};
export default api;
export type Api = typeof api;
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 'moex-chart';
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;
}
}
import { IndicativeOptionsResponse } from '@widgets/IndicativeQuotes/types/indicativeQuotesFiltersTypes';
import {
CreateQuote,
GetCandlesRequestParams,
GetCandlesResponse,
GroupingDataResponse,
HistoryDataParams,
HistoryDataResponse,
IndicativeQuote,
IndicativeQuotesGroupParams,
IndicativeQuotesRequestParams,
UpdateQuote,
} from 'types/IndicativeQuotes/indicativeQuotes';
import { QuotesType } from 'types/Quotes';
import { axiosIndicative, axiosInstanceNTPro, axiosInstanceTD } from '../axios';
import type { AxiosResponse } from 'axios';
const createNewQuote = {
bid: 0,
bidYield: 0,
bidQuantity: 0,
ask: 0,
askYield: 0,
askQuantity: 0,
secId: '',
secTypeId: 0,
settlement: '',
};
const getAxiosInstance = (quotesType: string) => {
switch (quotesType) {
case 'MOEX ISS': {
return axiosInstanceTD;
}
case 'NTPRO': {
return axiosInstanceNTPro;
}
case 'IndicativeQuotes': {
return axiosIndicative;
}
default:
return axiosInstanceTD;
}
};
export const indicativeQuotesController = {
/** Получение списка текущих индикативных котировок фирмы пользователя */
async getUserData(params: IndicativeQuotesRequestParams): Promise<AxiosResponse<{ quotes: IndicativeQuote[] }>> {
return axiosIndicative.get('/api/v1/quotes/indicative/user', { params });
},
async createNewQuotes(params: CreateQuote): Promise<AxiosResponse<{ quotes: IndicativeQuote[] }>> {
return axiosIndicative.post('/api/v1/quotes/indicative', { ...createNewQuote, ...params });
},
/** Получение значений для фильтров */
async getFilterOptions(): Promise<AxiosResponse<IndicativeOptionsResponse>> {
return axiosIndicative.get('/api/v1/quotes/indicative/options');
},
/** Получение значений для множественных фильтров */
async getFilterMultipleOptions(): Promise<AxiosResponse<IndicativeOptionsResponse>> {
return axiosIndicative.post('/api/v1/quotes/indicative/options');
},
/** Получение файла шаблона */
async getFileTemplate(): Promise<AxiosResponse<Blob>> {
return axiosIndicative.get('/api/v1/quotes/indicative/template', { responseType: 'blob' });
},
/** Получение списка текущих индикативных котировок */
async getGroupedData(params: IndicativeQuotesGroupParams): Promise<AxiosResponse<GroupingDataResponse>> {
return axiosIndicative.post('/api/v1/quotes/indicative/groups', { ...params });
},
/** Обновление */
async updateQuotes(params: UpdateQuote): Promise<AxiosResponse<{ quotes: IndicativeQuote[] }>> {
return axiosIndicative.put('/api/v1/quotes/indicative', params);
},
/** Исторические данные */
async getHistoryData(params: HistoryDataParams): Promise<AxiosResponse<HistoryDataResponse>> {
return axiosIndicative.get('/api/v1/quotes/indicative/history', { params });
},
async getCandles(params: GetCandlesRequestParams): Promise<AxiosResponse<GetCandlesResponse>> {
return axiosIndicative.get('/api/v1/candles/list/offset', { params });
},
async removeQuotesData(params: { ids: number[] }) {
return axiosIndicative.post('/api/v1/quotes/indicative/delete', params);
},
async getQuotes(type: string, keys: string[]): Promise<AxiosResponse<QuotesType[]>> {
const keysWithDots = keys.map((key) => key.replaceAll(':', '.'));
const axiosInstance = getAxiosInstance(type);
return axiosInstance.put('/api/v1/quotes', keysWithDots);
},
async getQuotesHistory(type: string, keys: string[], date: string): Promise<AxiosResponse<QuotesType[]>> {
const keysWithDots = keys.map((key) => key.replaceAll(':', '.'));
const axiosInstance = getAxiosInstance(type);
return axiosInstance.post('/api/v1/quotes/history', { keys: keysWithDots, date });
},
};
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import { getStartTime, parseTimeframe } from 'moex-chart';
import {
getISSOddTimeframePart,
moexChartTimeConverter,
moexChartToIssTimeframe,
} from '@utils/chartToReqTimeConverter';
import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
import { requestBars, requestRealtimeBars } from '../../requestBars';
import { ChartIndicativeData } from '../../types';
import type { ManipulateType } from 'dayjs';
import type { Candle, Timeframes } from 'moex-chart';
dayjs.extend(duration);
interface HistoryRequestState {
untilTime?: number;
request: Promise<Candle[] | null> | null;
}
function getRequestSymbol(symbolRaw?: string): string | undefined {
const symbol = String(symbolRaw ?? '').trim();
if (!symbol || symbol === DEFAULT_SYMBOL) {
return undefined;
}
return symbol;
}
const collectCandleGroup = ({
data,
startIndex,
iterationCounter,
startTime,
}: {
data: Candle[];
startIndex: number;
iterationCounter: number;
startTime: number;
}): {
aggregatedCandle: Candle | undefined;
consumedCount: number;
} => {
let aggregatedCandle: Candle | undefined;
let offset = 0;
let consumedCount = 0;
while (offset < iterationCounter) {
const currentCandle = data[startIndex + offset];
if (!currentCandle) {
break;
}
if (!aggregatedCandle) {
aggregatedCandle = currentCandle;
} else {
aggregatedCandle = {
...aggregatedCandle,
high: Math.max(currentCandle.high, aggregatedCandle.high),
low: Math.min(currentCandle.low, aggregatedCandle.low),
close: currentCandle.close,
volume: (aggregatedCandle.volume ?? 0) + (currentCandle.volume ?? 0),
};
}
consumedCount += 1;
if (startIndex + offset + 1 === data.length || currentCandle.time > startTime * 1000) {
break;
}
offset += 1;
}
return {
aggregatedCandle,
consumedCount,
};
};
const proceedConvolution = ({
data,
moexCandle,
requestedTimeframe,
dayjsUnit,
iterationCounter,
}: {
data: Candle[];
moexCandle: number;
requestedTimeframe: Timeframes;
dayjsUnit: ManipulateType;
iterationCounter: number;
}): Candle[] => {
const result: Candle[] = [];
let index = 0;
while (index < data.length - 1) {
const firstCandle = data[index];
if (!firstCandle) {
break;
}
const startTime = getStartTime(
requestedTimeframe,
dayjs(firstCandle.time).add(moexCandle, dayjsUnit).unix() * 1000,
);
const { aggregatedCandle, consumedCount } = collectCandleGroup({
data,
startIndex: index,
iterationCounter,
startTime,
});
if (!aggregatedCandle || consumedCount === 0) {
break;
}
result.push(aggregatedCandle);
index += consumedCount;
}
return result;
};
const timeframeConvolution = (data: Candle[], requestedTimeframe: Timeframes): Candle[] => {
const issTimeframe = moexChartToIssTimeframe(requestedTimeframe);
if (issTimeframe === requestedTimeframe) {
return data;
}
const { candleWidth: moexCandle, dayjsUnit } = parseTimeframe(requestedTimeframe);
const firstCandle = data[0];
if (!firstCandle) {
return [];
}
const dataStartTime = getStartTime(
requestedTimeframe,
dayjs(firstCandle.time).add(moexCandle, dayjsUnit).unix() * 1000,
);
const { value, unit } = getISSOddTimeframePart(issTimeframe);
let startIndex = 0;
while (
startIndex < data.length &&
startIndex < 60 &&
dataStartTime !== dayjs(data[startIndex].time).subtract(value, unit).unix()
) {
startIndex += 1;
}
if (startIndex >= data.length || startIndex >= 60) {
return [];
}
const { candleWidth: issCandle } = parseTimeframe(issTimeframe);
return proceedConvolution({
data: data.slice(startIndex),
moexCandle,
requestedTimeframe,
dayjsUnit,
iterationCounter: moexCandle / issCandle,
});
};
// По хорошему - класс должен быть синглтоном, чтобы кормить MoexChart одинаковой датой,
// и не плодить несколько подключений на одни символа
class DataSourceProvider {
private prevRealtimeDataArr: Candle[] = [];
private prevRealtimeData: Candle | undefined;
private realtimeShouldBeConvoluted = false;
private realtimeTimer: ReturnType<typeof setInterval> | null = null;
private historyRequests = new Map<string, HistoryRequestState>();
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;
}
const historyRequestKey = `${symbol}:${timeframe}`;
const historyRequestState = this.historyRequests.get(historyRequestKey);
if (historyRequestState && historyRequestState.untilTime === until?.time) {
if (historyRequestState.request) {
return historyRequestState.request;
}
if (until) {
return null;
}
}
cb?.(timeframe);
const historyRequest = this.requestHistoryData({
timeframe,
symbol,
until,
indicativeData,
});
this.historyRequests.set(historyRequestKey, {
untilTime: until?.time,
request: historyRequest,
});
try {
const data = await historyRequest;
return data;
} finally {
if (this.historyRequests.get(historyRequestKey)?.request === historyRequest) {
if (until) {
this.historyRequests.set(historyRequestKey, {
untilTime: until.time,
request: null,
});
} else {
this.historyRequests.delete(historyRequestKey);
}
}
}
};
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();
const symbolIds = getSymbols();
Promise.all(
symbolIds.map(async (symbolId) => {
const symbol = getRequestSymbol(symbolId);
if (!symbol) {
return;
}
const data = await requestRealtimeBars({
currencyPair: symbol.replaceAll(':', '.'),
interval: moexChartTimeConverter(timeframe),
ticker: symbol,
indicativeData,
});
if (!data) {
return;
}
if (!this.prevRealtimeData) {
this.prevRealtimeData = data;
this.prevRealtimeDataArr = [data];
update(symbol, data);
return;
}
if (JSON.stringify(data) !== JSON.stringify(this.prevRealtimeData)) {
this.realtimeConvolution(timeframe, data, (candle) => {
update(symbol, candle);
});
}
}),
);
}, periodMs);
return () => {
if (this.realtimeTimer) {
clearInterval(this.realtimeTimer);
}
this.realtimeTimer = null;
};
}
private async requestHistoryData({
timeframe,
symbol,
until,
indicativeData,
}: {
timeframe: Timeframes;
symbol: string;
until?: Candle;
indicativeData?: ChartIndicativeData;
}): Promise<Candle[] | null> {
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: Date.now(),
countBack: 2000,
},
ticker: symbol,
indicativeData,
});
if (data.length === 0) {
return null;
}
const issTimeframe = moexChartToIssTimeframe(timeframe);
if (issTimeframe === timeframe) {
this.realtimeShouldBeConvoluted = false;
return data;
}
this.realtimeShouldBeConvoluted = true;
return timeframeConvolution(data, timeframe);
}
private realtimeConvolution(timeframe: Timeframes, data: Candle, update: (candle: Candle) => void): void {
let candle = data;
if (this.realtimeShouldBeConvoluted && this.prevRealtimeData) {
const { candleWidth: moexCandle, dayjsUnit } = parseTimeframe(timeframe);
const dataStartTimeFromPrevData = getStartTime(
timeframe,
dayjs(this.prevRealtimeData.time).add(moexCandle, dayjsUnit).unix() * 1000,
);
const dataStartTime = getStartTime(timeframe, dayjs(data.time).add(moexCandle, dayjsUnit).unix() * 1000);
const isNextTimeframeStep = dataStartTime !== dataStartTimeFromPrevData;
if (isNextTimeframeStep) {
this.prevRealtimeDataArr = [data];
} else {
const isNewBarInsideTimeframe = this.prevRealtimeData.time !== data.time;
if (isNewBarInsideTimeframe) {
this.prevRealtimeDataArr.push(data);
} else {
this.prevRealtimeDataArr[this.prevRealtimeDataArr.length - 1] = data;
}
const firstCandle = this.prevRealtimeDataArr[0];
const lastCandle = this.prevRealtimeDataArr[this.prevRealtimeDataArr.length - 1];
if (firstCandle && lastCandle) {
candle = {
time: firstCandle.time,
open: firstCandle.open,
high: Math.max(...this.prevRealtimeDataArr.map(({ high }) => high)),
low: Math.min(...this.prevRealtimeDataArr.map(({ low }) => low)),
close: lastCandle.close,
volume: this.prevRealtimeDataArr.reduce((total, currentCandle) => total + (currentCandle.volume ?? 0), 0),
};
}
}
}
this.prevRealtimeData = data;
update(candle);
}
}
export { DataSourceProvider };
import { Intervals, MoexChart, Timeframes } from 'moex-chart';
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__, IMoexChart, SymbolInfoInput } from 'moex-chart';
interface TUseMoexChartProps {
symbolInfo: SymbolInfoInput;
indicativeData?: ChartIndicativeData;
}
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 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: Timeframes,
initialInterval?: Intervals,
): IMoexChart['snapshot'] => ({
...snapshot,
charts: snapshot.charts.map((chartSnapshot) => ({
...chartSnapshot,
...currentSymbolInfoRef.current,
timeframe,
...(initialInterval ? { interval: initialInterval } : {}),
})),
});
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 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, initialInterval),
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) {
initialIntervalRef.current = undefined;
updateProperties((state) => {
state.moexChartState = {
...state.moexChartState,
initialInterval: 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 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 'moex-chart';
import 'moex-chart/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 React, { FC, lazy, useContext, useState } from 'react';
import { DNDWrapper } from '@components/DNDWrapper';
import { InstrumentSearch } from '@components/InstrumentSearch';
import WidgetContentWrapper from '@components/WidgetContentWrapper';
import WidgetHeader from '@components/WidgetHeader';
import { useDropInstrument } from '@hooks/dnd';
import { WidgetEnvContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { useWidgetHeaderName } from '@utils/useWidgetName';
import useChartComponentFacade from './hooks/useChartComponentFacade';
import type { ChartContainerProps } from './types';
const MoexChart = lazy(() => import('./components/MoexChart/MoexChart'));
export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
const indicativeData = props.widgetContentProps?.indicativeData;
const {
dropDownOpen,
setDropdownOpen,
currentSymbolInfo,
setIsWidgetHeaderContextMenuOpen,
isWidgetHeaderContextMenuOpen,
onDropInstrument,
addInstrumentFromModal,
isOver,
} = useChartComponentFacade(props);
/* на дроп обновляем название бумаги в сторе
и делаем апдейт на бэк, чтобы сохранить изменения
при обновлении страницы */
const { dropRef } = useDropInstrument((dragData) => {
onDropInstrument(dragData.properties.issKey, true);
});
const [isOpenEmptyAction, setIsOpenEmptyAction] = useState<boolean>(false);
const openInstrumentModal = () => {
setIsOpenEmptyAction(true);
setIsWidgetHeaderContextMenuOpen(false);
};
// TODO: Зависит от сеток
const { isDraggedOver } = useContext(WidgetEnvContext);
const contractsInstrumentName = useWidgetHeaderName(currentSymbolInfo.symbolId);
const symbolInfo = indicativeData
? {
symbolId: indicativeData.key,
symbol: indicativeData.secId,
symbolName: `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`,
}
: currentSymbolInfo;
const instrumentName = symbolInfo.symbolName || contractsInstrumentName || symbolInfo.symbol || symbolInfo.symbolId;
return (
<DNDWrapper
ref={dropRef}
isOver={isOver}
canDrop
>
<WidgetHeader
{...props}
dropdownOpen={dropDownOpen}
setDropdownOpen={setDropdownOpen}
handlerSaveAsExcel={null}
addToWidgetNamePrefix={instrumentName}
setIsOpenContextMenuFromWidget={setIsWidgetHeaderContextMenuOpen}
isOpenContextMenuFromWidget={isWidgetHeaderContextMenuOpen}
openInstrumentsModal={openInstrumentModal}
/>
<WidgetContentWrapper {...props}>
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
pointerEvents: isDraggedOver ? 'none' : 'inherit',
}}
>
<MoexChart
symbolInfo={symbolInfo}
indicativeData={indicativeData}
widgetId={props.widgetId}
addInstrumentFromModal={addInstrumentFromModal}
/>
{isOpenEmptyAction && (
<InstrumentSearch
setOpen={setIsOpenEmptyAction}
isOpen={isOpenEmptyAction}
variant="single"
widgetId={props.widgetId}
// withNRD={false}
addInstruments={addInstrumentFromModal}
/>
)}
</div>
</WidgetContentWrapper>
</DNDWrapper>
);
};
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 { getState } 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 'moex-chart';
import type { Dispatch, 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;
};
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>({
symbolId: widgetProperties?.chartState?.savedInstrument ?? DEFAULT_SYMBOL,
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 = (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 onInstrumentChange = useCallback(
(symbolInfo: SymbolInfoInput, withUpdate?: boolean, unbind = true): void => {
const symbolId = symbolInfo.symbolId.trim();
if (!symbolId) {
return;
}
setIsWidgetHeaderContextMenuOpen(false);
const currentValue = currentSymbolInfoRef.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;
}
currentSymbolInfoRef.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],
);
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],
);
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 type { WidgetProperties } from './properties/types';
import type { Contract } from '@modules/contracts';
import type { WidgetProperties as BaseWidgetProperties } from '@modules/widgetProperties/types';
import type { WidgetContentBasicProps } from 'types/Widgets';
export interface ChartIndicativeData extends BaseWidgetProperties {
secId: string;
instrumentName: string;
settlement: string;
firmName: string;
key: string;
}
export interface SelectedInstrument {
issKey: Contract['issKey'];
displayName: Contract['displayName'];
symbol: Contract['symbol'];
}
export type ChartContainerProps = WidgetContentBasicProps<WidgetProperties>;