/** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import type Nullable from '../common/Nullable'; import type ExcludePickPartial from '../common/ExcludePickPartial'; import type TViewData from '../common/TViewData'; import type Bounding from '../common/Bounding'; import type VisibleRange from '../common/VisibleRange'; import type BarSpace from '../common/BarSpace'; import type Crosshair from '../common/Crosshair'; import { type IndicatorStyle, type IndicatorPolygonStyle, type SmoothLineStyle, type RectStyle, type TextStyle, type TooltipIconStyle, type LineStyle, type LineType, type PolygonType, type TooltipLegend, } from '../common/Styles'; import { isNumber, isValid, merge, clone, isArray, isBoolean, } from '../common/utils/typeChecks'; import { type XAxis } from './XAxis'; import { type YAxis } from './YAxis'; import { formatValue } from '../common/utils/format'; import { type ArcAttrs } from '../extension/figure/arc'; import { type RectAttrs } from '../extension/figure/rect'; import { type TextAttrs } from '../extension/figure/text'; export enum IndicatorSeries { Normal = 'normal', Price = 'price', Volume = 'volume', } export type IndicatorFigureStyle = Partial> & Partial> & Partial & Partial<{ style: LineType[keyof LineType] | PolygonType[keyof PolygonType]; }> & Record; export type IndicatorFigureAttrs = Partial & Partial & Partial & Partial & Record; export interface IndicatorFigureCallbackBrother { prev: PCN; current: PCN; next: PCN; } export type IndicatorFigureAttrsCallbackCoordinate = IndicatorFigureCallbackBrother & { x: number }>; export type IndicatorFigureAttrsCallbackData = IndicatorFigureCallbackBrother; export interface IndicatorFigureAttrsCallbackParams { data: IndicatorFigureAttrsCallbackData>; coordinate: IndicatorFigureAttrsCallbackCoordinate; bounding: Bounding; barSpace: BarSpace; xAxis: XAxis; yAxis: YAxis; } export interface IndicatorFigureStylesCallbackDataChild { TViewData?: TViewData; indicatorData?: D; } export type IndicatorFigureStylesCallbackData = IndicatorFigureCallbackBrother>; export type IndicatorFigureAttrsCallback = ( params: IndicatorFigureAttrsCallbackParams, ) => IndicatorFigureAttrs; export type IndicatorFigureStylesCallback = ( data: IndicatorFigureStylesCallbackData, indicator: Indicator, defaultStyles: IndicatorStyle, ) => IndicatorFigureStyle; export interface IndicatorFigure { key: string; title?: string; type?: string; baseValue?: number; attrs?: IndicatorFigureAttrsCallback; styles?: IndicatorFigureStylesCallback; } export type IndicatorShouldUpdateReturn = | boolean | { calc: boolean; draw: boolean }; export type IndicatorRegenerateFiguresCallback = ( calcParams: any[], ) => Array>; export interface IndicatorTooltipData { name: string; calcParamsText: string; icons: TooltipIconStyle[]; values: TooltipLegend[]; } export interface IndicatorCreateTooltipDataSourceParams { TViewDataList: TViewData[]; indicator: Indicator; visibleRange: VisibleRange; bounding: Bounding; crosshair: Crosshair; defaultStyles: IndicatorStyle; xAxis: XAxis; yAxis: YAxis; } export type IndicatorCreateTooltipDataSourceCallback = ( params: IndicatorCreateTooltipDataSourceParams, ) => IndicatorTooltipData; export interface IndicatorDrawParams { ctx: CanvasRenderingContext2D; TViewDataList: TViewData[]; indicator: Indicator; visibleRange: VisibleRange; bounding: Bounding; barSpace: BarSpace; defaultStyles: IndicatorStyle; xAxis: XAxis; yAxis: YAxis; } export type IndicatorDrawCallback = ( params: IndicatorDrawParams, ) => boolean; export type IndicatorCalcCallback = ( dataList: TViewData[], indicator: Indicator, ) => Promise | D[]; export interface Indicator { /** * Indicator name */ name: string; /** * Short name, for display */ shortName: string; /** * Precision */ precision: number; /** * Calculation parameters */ calcParams: any[]; /** * Whether ohlc column is required */ shouldOhlc: boolean; /** * Whether large data values need to be formatted, starting from 1000, for example, whether 100000 needs to be formatted with 100K */ shouldFormatBigNumber: boolean; /** * Whether the indicator is visible */ visible: boolean; /** * Z index */ zLevel: number; /** * Extend data */ extendData: any; /** * Indicator series */ series: IndicatorSeries; /** * Figure configuration information */ figures: Array>; /** * Specified minimum value */ minValue: Nullable; /** * Specified maximum value */ maxValue: Nullable; /** * Style configuration */ styles: Nullable>; /** * Should update, should calc or draw */ shouldUpdate: ( prev: Indicator, current: Indicator, ) => IndicatorShouldUpdateReturn; /** * Indicator calculation */ calc: IndicatorCalcCallback; /** * Regenerate figure configuration */ regenerateFigures: Nullable>; /** * Create custom tooltip text */ createTooltipDataSource: Nullable; /** * Custom draw */ draw: Nullable>; /** * Calculation result */ result: D[]; } export type IndicatorTemplate = ExcludePickPartial< Omit, 'result'>, 'name' | 'calc' >; export type IndicatorCreate = ExcludePickPartial< Omit, 'result'>, 'name' >; export type IndicatorConstructor = new () => IndicatorImp; export type EachFigureCallback = ( figure: IndicatorFigure, figureStyles: IndicatorFigureStyle, index: number, ) => void; export function eachFigures( TViewDataList: TViewData[], indicator: Indicator, dataIndex: number, defaultStyles: IndicatorStyle, eachFigureCallback: EachFigureCallback, ): void { const result = indicator.result; const figures = indicator.figures; const styles = indicator.styles; const circleStyles = formatValue( styles, 'circles', defaultStyles.circles, ) as IndicatorPolygonStyle[]; const circleStyleCount = circleStyles.length; const barStyles = formatValue( styles, 'bars', defaultStyles.bars, ) as IndicatorPolygonStyle[]; const barStyleCount = barStyles.length; const lineStyles = formatValue( styles, 'lines', defaultStyles.lines, ) as SmoothLineStyle[]; const lineStyleCount = lineStyles.length; let circleCount = 0; let barCount = 0; let lineCount = 0; let defaultFigureStyles; let figureIndex = 0; figures.forEach((figure) => { switch (figure.type) { case 'circle': { figureIndex = circleCount; const styles = circleStyles[circleCount % circleStyleCount]; defaultFigureStyles = { ...styles, color: styles.noChangeColor }; circleCount++; break; } case 'bar': { figureIndex = barCount; const styles = barStyles[barCount % barStyleCount]; defaultFigureStyles = { ...styles, color: styles.noChangeColor }; barCount++; break; } case 'line': { figureIndex = lineCount; defaultFigureStyles = lineStyles[lineCount % lineStyleCount]; lineCount++; break; } default: { break; } } if (isValid(defaultFigureStyles)) { const cbData = { prev: { TViewData: TViewDataList[dataIndex - 1], indicatorData: result[dataIndex - 1], }, current: { TViewData: TViewDataList[dataIndex], indicatorData: result[dataIndex], }, next: { TViewData: TViewDataList[dataIndex + 1], indicatorData: result[dataIndex + 1], }, }; const ss = figure.styles?.(cbData, indicator, defaultStyles); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument eachFigureCallback( figure, { ...defaultFigureStyles, ...ss }, figureIndex, ); } }); } export default class IndicatorImp { private _prevIndicator: Indicator; private readonly _indicator: Indicator = { name: '', shortName: '', precision: 4, calcParams: [], shouldOhlc: false, shouldFormatBigNumber: false, visible: true, zLevel: 0, extendData: null, series: IndicatorSeries.Normal, figures: [], minValue: null, maxValue: null, styles: {}, regenerateFigures: null, createTooltipDataSource: null, shouldUpdate: (prev, current) => { const calc = JSON.stringify(prev.calcParams) !== JSON.stringify(current.calcParams) || prev.figures !== current.figures || prev.calc !== current.calc; const draw = calc || prev.shortName !== current.shortName || prev.series !== current.series || prev.minValue !== current.minValue || prev.maxValue !== current.maxValue || prev.precision !== current.precision || prev.shouldOhlc !== current.shouldOhlc || prev.shouldFormatBigNumber !== current.shouldFormatBigNumber || prev.visible !== current.visible || prev.zLevel !== current.zLevel || prev.extendData !== current.extendData || prev.regenerateFigures !== current.regenerateFigures || prev.createTooltipDataSource !== current.createTooltipDataSource || prev.draw !== current.draw; return { calc, draw }; }, calc: () => [], draw: null, result: [], }; private _lockSeriesPrecision: boolean = false; constructor(indicator: IndicatorTemplate) { this.override(indicator); this._indicator.shortName ??= this._indicator.name; if (isArray(indicator.figures)) { this._indicator.figures = indicator.figures; } } getIndicator(): Indicator { return this._indicator; } override(indicator: IndicatorCreate): void { this._prevIndicator = clone(this._indicator); merge(this._indicator, indicator); if (isNumber(indicator.precision)) { this._lockSeriesPrecision = true; } } setSeriesPrecision(precision: number): void { if (!this._lockSeriesPrecision) { this._indicator.precision = precision; } } shouldUpdate(): { calc: boolean; draw: boolean; sort: boolean } { const sort = this._prevIndicator.zLevel !== this._indicator.zLevel; const result = this._indicator.shouldUpdate( this._prevIndicator, this._indicator, ); if (isBoolean(result)) { return { calc: result, draw: result, sort }; } return { ...result, sort }; } async calc(dataList: TViewData[]): Promise { try { const result = await this._indicator.calc(dataList, this._indicator); this._indicator.result = result; return true; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return false; } } static extend(template: IndicatorTemplate): IndicatorConstructor { class Custom extends IndicatorImp { constructor() { super(template); } } return Custom; } }