Загрузка данных
import {
BaselineSeries,
type IPriceLine,
LineStyle,
type SeriesDefinition,
type SeriesPartialOptionsMap,
type SingleValueData,
type Time,
} from 'lightweight-charts';
import { SingleValueSeriesStrategy } from '@core/Series/SingleValueSeriesStrategy';
import { getThemeStore } from '@src/theme/store';
import type { BaseSeriesParams } from '@core/Series/BaseSeries';
const BASE_LEVEL = 0.5;
export class BaselineSeriesStrategy extends SingleValueSeriesStrategy<'Baseline'> {
private basePriceLine: IPriceLine | null = null;
private updateFrame: number | null = null;
constructor(params: BaseSeriesParams<'Baseline'>) {
super(params);
const timeScale = this.lwcChart.timeScale();
timeScale.subscribeVisibleLogicalRangeChange(this.scheduleBaseValueUpdate);
timeScale.subscribeSizeChange(this.scheduleBaseValueUpdate);
this.subscribeDataChanged(this.scheduleBaseValueUpdate);
this.scheduleBaseValueUpdate();
}
public destroy(): void {
const timeScale = this.lwcChart.timeScale();
timeScale.unsubscribeVisibleLogicalRangeChange(this.scheduleBaseValueUpdate);
timeScale.unsubscribeSizeChange(this.scheduleBaseValueUpdate);
this.unsubscribeDataChanged(this.scheduleBaseValueUpdate);
this.cancelScheduledUpdate();
this.removeBasePriceLine();
super.destroy();
}
public getTypeName(): string {
return 'Baseline';
}
protected seriesDefinition(): SeriesDefinition<'Baseline'> {
return BaselineSeries;
}
protected getDefaultOptions(): SeriesPartialOptionsMap['Baseline'] {
return {
relativeGradient: true,
};
}
protected getLegendColor(currentBar: SingleValueData<Time>): string {
const options = this.options();
return currentBar.value >= options.baseValue.price ? options.topLineColor : options.bottomLineColor;
}
private scheduleBaseValueUpdate = (): void => {
this.cancelScheduledUpdate();
this.updateFrame = requestAnimationFrame(() => {
this.updateFrame = null;
this.updateBaseValue();
});
};
private updateBaseValue(): void {
const paneHeight = this.getPane().getHeight();
if (paneHeight <= 0) {
return;
}
const basePrice = this.coordinateToPrice(paneHeight * BASE_LEVEL);
if (basePrice === null) {
return;
}
this.applyOptions({
baseValue: {
type: 'price',
price: basePrice,
},
});
this.updateBasePriceLine(basePrice);
}
private updateBasePriceLine(price: number): void {
if (this.basePriceLine) {
this.basePriceLine.applyOptions({ price });
return;
}
const { colors } = getThemeStore();
this.basePriceLine = this.createPriceLine({
price,
color: colors.chartCrosshairLine,
lineWidth: 1,
lineStyle: LineStyle.Dashed,
axisLabelVisible: false,
});
}
private cancelScheduledUpdate(): void {
if (this.updateFrame === null) {
return;
}
cancelAnimationFrame(this.updateFrame);
this.updateFrame = null;
}
private removeBasePriceLine(): void {
if (!this.basePriceLine) {
return;
}
this.removePriceLine(this.basePriceLine);
this.basePriceLine = null;
}
}
import { type BehaviorSubject, distinctUntilChanged, type Observable, Subscription } from 'rxjs';
import { MAIN_PANE_INDEX } from '@src/constants';
import { type Candle } from '@src/types';
import { formatPrice, getPricePrecisionStep, isBarData, normalizeSeriesData } from '@src/utils';
import { isSingleValueData } from '@src/utils/isSingleValueData';
import type { DataSource } from '@core/DataSource';
import type { Indicator } from '@core/Indicator';
import type { ChartTypeToCandleData, IndicatorDataFormatter } from '@core/Indicators';
import type { Ohlc } from '@core/Legend';
import type { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import type {
BarData,
BarPrice,
BarsInfo,
Coordinate,
CreatePriceLineOptions,
CustomData,
DataChangedHandler,
DeepPartial,
HistogramData,
IChartApi,
IPaneApi,
IPriceFormatter,
IPriceLine,
IPriceScaleApi,
IRange,
ISeriesApi,
ISeriesPrimitive,
LineData,
MismatchDirection,
MouseEventParams,
PriceScaleOptions,
SeriesDataItemTypeMap,
SeriesDefinition,
SeriesOptionsMap,
SeriesPartialOptionsMap,
SeriesType,
Time,
} from 'lightweight-charts';
export interface SerieData {
time: Time;
customValues: Candle;
}
export interface CreateSeriesParams<TSeries extends SeriesType> {
chart: IChartApi;
seriesOptions?: SeriesPartialOptionsMap[TSeries];
paneIndex?: number;
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
}
export interface IBaseSeries<TSeries extends SeriesType> extends ISeriesApi<TSeries> {
getLwcSeries: () => ISeriesApi<TSeries>;
getLegendData: (param?: MouseEventParams) => Partial<
Record<
keyof Ohlc,
{
value: number | string | Time;
color: string;
name: string;
}
>
>;
}
export interface BaseSeriesParams<TSeries extends SeriesType = SeriesType> {
lwcChart: IChartApi;
dataSource: DataSource;
mainSymbolId$: Observable<string>;
mainSymbol$: Observable<string>;
mainSerie$: BehaviorSubject<SeriesStrategies | null>;
actLikeMainSerie?: boolean;
customFormatter?: (params: IndicatorDataFormatter<TSeries>) => SeriesDataItemTypeMap<Time>[TSeries][];
seriesOptions?: SeriesPartialOptionsMap[TSeries];
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
showSymbolLabel?: boolean;
paneIndex?: number;
indicatorReference?: Indicator;
}
export abstract class BaseSeries<TSeries extends SeriesType> implements IBaseSeries<TSeries> {
protected lwcSeries: ISeriesApi<TSeries>;
protected customFormatter?: (params: IndicatorDataFormatter<TSeries>) => SeriesDataItemTypeMap<Time>[TSeries][];
protected lwcChart: IChartApi;
protected mainSymbolId$: Observable<string>;
protected mainSymbol$: Observable<string>;
protected mainSerie$: BehaviorSubject<SeriesStrategies | null>;
protected paneIndex: number | null = null;
protected indicatorReference: Indicator | null = null;
protected showSymbolLabel: boolean;
private subscriptions = new Subscription();
private dataSub: Subscription | null = null;
private realtimeSub: Subscription | null = null;
public actLikeMainSerie = false;
constructor({
lwcChart,
mainSymbolId$,
mainSymbol$,
mainSerie$,
customFormatter,
seriesOptions,
priceScaleOptions,
showSymbolLabel = true,
paneIndex,
indicatorReference,
actLikeMainSerie,
}: BaseSeriesParams<TSeries>) {
this.lwcSeries = this.createSeries({
chart: lwcChart,
seriesOptions,
paneIndex,
priceScaleOptions,
});
this.actLikeMainSerie = actLikeMainSerie ?? false;
this.lwcChart = lwcChart;
this.customFormatter = customFormatter;
this.mainSymbolId$ = mainSymbolId$;
this.mainSymbol$ = mainSymbol$;
this.mainSerie$ = mainSerie$;
this.showSymbolLabel = showSymbolLabel;
this.indicatorReference = indicatorReference ?? null;
}
public getLegendData = (
param?: MouseEventParams,
): Partial<
Record<
keyof Ohlc,
{
value: number | string | Time;
color: string;
name: string;
}
>
> => {
if (!param) {
const seriesData = this.data();
const currentBar = seriesData[seriesData.length - 1];
if (!currentBar) {
return {};
}
return this.formatLegendValues(currentBar, seriesData[seriesData.length - 2] ?? null);
}
const currentBar = param.seriesData.get(this.lwcSeries) ?? null;
const previousBar = param.logical === null ? null : this.dataByIndex(param.logical! - 1);
return this.formatLegendValues(currentBar, previousBar);
};
public show(): void {
this.lwcSeries.applyOptions({
visible: true,
});
}
public hide(): void {
this.lwcSeries.applyOptions({
visible: false,
});
}
public isVisible(): boolean {
return this.lwcSeries.options().visible;
}
public destroy(): void {
this.dataSub?.unsubscribe();
this.realtimeSub?.unsubscribe();
this.subscriptions.unsubscribe();
this.lwcChart.removeSeries(this.lwcSeries);
}
public getLwcSeries(): ISeriesApi<TSeries> {
return this.lwcSeries;
}
public applyOptions(options: SeriesPartialOptionsMap[TSeries]): void {
this.lwcSeries.applyOptions(options);
}
public attachPrimitive(primitive: ISeriesPrimitive<Time>): void {
this.lwcSeries.attachPrimitive(primitive);
}
public barsInLogicalRange(range: IRange<number>): BarsInfo<Time> | null {
return this.lwcSeries.barsInLogicalRange(range);
}
public coordinateToPrice(coordinate: number): BarPrice | null {
return this.lwcSeries.coordinateToPrice(coordinate);
}
public createPriceLine(options: CreatePriceLineOptions): IPriceLine {
return this.lwcSeries.createPriceLine(options);
}
public data(): readonly SeriesDataItemTypeMap<Time>[TSeries][] {
return this.lwcSeries.data();
}
public pop(count: number): SeriesDataItemTypeMap<Time>[TSeries][] {
return this.lwcSeries.pop(count);
}
public dataByIndex(
logicalIndex: number,
mismatchDirection?: MismatchDirection,
): SeriesDataItemTypeMap<Time>[TSeries] | null {
return this.lwcSeries.dataByIndex(logicalIndex, mismatchDirection);
}
public detachPrimitive(primitive: ISeriesPrimitive<Time>): void {
this.lwcSeries.detachPrimitive(primitive);
}
public getPane(): IPaneApi<Time> {
return this.lwcSeries.getPane();
}
public moveToPane(paneIndex: number): void {
this.lwcSeries.moveToPane(paneIndex);
}
public options(): Readonly<SeriesOptionsMap[TSeries]> {
return this.lwcSeries.options();
}
public priceFormatter(): IPriceFormatter {
return this.lwcSeries.priceFormatter();
}
public priceLines(): IPriceLine[] {
return this.lwcSeries.priceLines();
}
public priceScale(): IPriceScaleApi {
return this.lwcSeries.priceScale();
}
public priceToCoordinate(price: number): Coordinate | null {
return this.lwcSeries.priceToCoordinate(price);
}
public removePriceLine(line: IPriceLine): void {
this.lwcSeries.removePriceLine(line);
}
public seriesOrder(): number {
return this.lwcSeries.seriesOrder();
}
public seriesType(): TSeries {
return this.lwcSeries.seriesType();
}
public lastValueData(globalLast: boolean): ReturnType<ISeriesApi<TSeries>['lastValueData']> {
return this.lwcSeries.lastValueData(globalLast);
}
public setData(data: SeriesDataItemTypeMap<Time>[TSeries][]): void {
this.lwcSeries.setData(normalizeSeriesData(data));
}
public setSeriesOrder(order: number): void {
this.lwcSeries.setSeriesOrder(order);
}
public subscribeDataChanged(handler: DataChangedHandler): void {
this.lwcSeries.subscribeDataChanged(handler);
}
public unsubscribeDataChanged(handler: DataChangedHandler): void {
this.lwcSeries.unsubscribeDataChanged(handler);
}
public update(bar: SeriesDataItemTypeMap<Time>[TSeries], historicalUpdate = false): void {
this.lwcSeries.update(bar, historicalUpdate);
}
protected updateRealtime(bar: SeriesDataItemTypeMap<Time>[TSeries]): void {
this.update(bar, true);
}
protected createSeries({
chart,
seriesOptions,
paneIndex = MAIN_PANE_INDEX,
priceScaleOptions = {},
}: CreateSeriesParams<TSeries>): ISeriesApi<TSeries> {
this.paneIndex = paneIndex;
const options = {
...this.getDefaultOptions(),
...seriesOptions,
};
const series = chart.addSeries<TSeries>(this.seriesDefinition(), options, paneIndex);
series.priceScale().applyOptions(priceScaleOptions);
return series;
}
protected abstract dataSourceSubscription(next: Candle[]): void;
protected abstract seriesDefinition(): SeriesDefinition<TSeries>;
protected abstract dataSourceRealtimeSubscription(next: Candle): void;
protected abstract getDefaultOptions(): SeriesPartialOptionsMap[TSeries];
protected abstract formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>[TSeries][];
protected abstract formatLegendValues(
currentBar: BarData | LineData | HistogramData | CustomData | null,
prevBar: BarData | LineData | HistogramData | CustomData | null,
): Partial<
Record<
keyof Ohlc,
{
value: number | string | Time;
color: string;
name: string;
}
>
>;
protected formatData(inputData: Candle[]): SeriesDataItemTypeMap<Time>[TSeries][] {
if (!this.customFormatter) {
return this.formatMainSerie(inputData);
}
const mainSeriesData = (this.mainSerie$.value?.data() ?? []) as unknown as SerieData[];
const selfData = this.data() as unknown as ChartTypeToCandleData[TSeries][];
if (inputData.length !== 1) {
return this.customFormatter({
mainSeriesData,
selfData,
indicatorReference: this.indicatorReference ?? undefined,
});
}
const candle = this.formatMainSerie(inputData)[0];
if (!candle) {
return [];
}
return this.customFormatter({
mainSeriesData,
selfData,
candle: candle as unknown as SerieData,
indicatorReference: this.indicatorReference ?? undefined,
});
}
protected subscribeDataSource = (dataSource: DataSource): void => {
const minMove = getPricePrecisionStep();
this.lwcSeries.applyOptions({
priceFormat: {
type: 'custom',
minMove,
formatter: (price: number) => formatPrice(price) ?? String(price),
},
});
this.subscriptions.add(
this.mainSymbol$.pipe(distinctUntilChanged()).subscribe((symbol) => {
this.lwcSeries.applyOptions({
title: this.showSymbolLabel ? symbol : '',
});
}),
);
this.subscriptions.add(
this.mainSymbolId$.pipe(distinctUntilChanged()).subscribe((symbolId) => {
this.dataSub?.unsubscribe();
this.realtimeSub?.unsubscribe();
this.dataSub = dataSource.subscribe(symbolId, (next) => {
this.dataSourceSubscription(next);
});
this.realtimeSub = dataSource.subscribeRealtime(symbolId, (next: Candle) => {
this.dataSourceRealtimeSubscription(next);
});
}),
);
};
}
export function calcCandleChange(
prev: BarData | LineData | HistogramData | CustomData | null,
current: BarData | LineData | HistogramData | CustomData | null,
):
| (Ohlc & {
customValues?: Record<string, unknown>;
})
| null {
if (!current) {
return null;
}
if (!prev) {
return current;
}
if (isBarData(prev) && isBarData(current)) {
const absoluteChange = current.close - prev.close;
const percentageChange = ((current.close - prev.close) / prev.close) * 100;
return {
...current,
absoluteChange,
percentageChange: Number.isNaN(percentageChange) ? 0 : percentageChange,
};
}
if (isSingleValueData(prev) && isSingleValueData(current)) {
const absoluteChange = current.value - prev.value;
const percentageChange = ((current.value - prev.value) / prev.value) * 100;
const high = current.customValues?.high;
const low = current.customValues?.low;
return {
time: current.time,
value: current.value,
high: typeof high === 'number' ? high : current.value,
low: typeof low === 'number' ? low : current.value,
absoluteChange,
percentageChange: Number.isNaN(percentageChange) ? 0 : percentageChange,
customValues: current.customValues,
};
}
return null;
}
import { BaseSeries, type BaseSeriesParams, calcCandleChange } from '@core/Series/BaseSeries';
import { t } from '@src/translations';
import { formatCompactNumber } from '@src/utils';
import { isSingleValueData } from '@src/utils/isSingleValueData';
import { removeAlphaFromHex } from '@src/utils/removeAlphaFromHex';
import type { Ohlc } from '@core/Legend';
import type { ISeries } from '@src/modules/series-strategies';
import type { Candle, LineCandle } from '@src/types';
import type {
BarData,
CustomData,
HistogramData,
LineData,
SeriesDataItemTypeMap,
SingleValueData,
Time,
} from 'lightweight-charts';
type SingleValueSeriesType = 'Line' | 'Histogram' | 'Baseline';
type SourceData = Partial<Candle> & Partial<LineCandle>;
type LegendData = BarData | LineData | HistogramData | CustomData | null;
type LegendValues = Partial<
Record<
keyof Ohlc,
{
value: number | string | Time;
color: string;
name: string;
}
>
>;
export abstract class SingleValueSeriesStrategy<TSeries extends SingleValueSeriesType>
extends BaseSeries<TSeries>
implements ISeries<TSeries>
{
constructor(params: BaseSeriesParams<TSeries>) {
super(params);
this.subscribeDataSource(params.dataSource);
}
public validateData(data: SourceData[]): boolean {
return Array.isArray(data) && data.every((point) => typeof point.time === 'number');
}
protected dataSourceSubscription = (dataToSet: Candle[]): void => {
if (!this.validateData(dataToSet)) {
console.error(`LightweightAPI: Invalid data format for ${this.getTypeName()} chart`);
return;
}
this.setData(this.formatData(dataToSet));
};
protected dataSourceRealtimeSubscription = (dataToSet: Candle): void => {
if (!this.validateData([dataToSet])) {
console.error(`LightweightAPI: Invalid data format for ${this.getTypeName()} chart`);
return;
}
const [formattedData] = this.formatData([dataToSet]);
if (!formattedData) {
return;
}
this.updateRealtime(formattedData);
};
protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>[TSeries][] {
return inputData.map((point) => this.formatPoint(point));
}
protected formatLegendValues(currentBar: LegendData, prevBar: LegendData): LegendValues {
if (!isSingleValueData(currentBar)) {
return {};
}
const change = calcCandleChange(prevBar, currentBar);
if (!change) {
return {};
}
const color = this.getLegendColor(currentBar);
return {
value: {
value: formatCompactNumber(currentBar.value) ?? '',
name: '',
color,
},
absoluteChange: {
value: formatCompactNumber(change.absoluteChange ?? 0),
name: t('Change'),
color,
},
percentageChange: {
value: change.percentageChange !== undefined ? `${formatCompactNumber(change.percentageChange)}%` : '',
name: t('Change'),
color,
},
time: {
value: change.time,
name: t('Time'),
color,
},
};
}
protected getLegendColor(currentBar: SingleValueData<Time>): string {
if ('color' in currentBar && typeof currentBar.color === 'string') {
return removeAlphaFromHex(currentBar.color);
}
const options = this.options();
if ('color' in options && typeof options.color === 'string') {
return options.color;
}
return '';
}
private formatPoint(point: SourceData): SeriesDataItemTypeMap<Time>[TSeries] {
const time = point.time as Time;
const value = typeof point.close === 'number' ? point.close : point.value;
if (typeof value !== 'number') {
return {
time,
} as SeriesDataItemTypeMap<Time>[TSeries];
}
return {
time,
value,
customValues: point as unknown as Record<string, unknown>,
} as SeriesDataItemTypeMap<Time>[TSeries];
}
public abstract getTypeName(): string;
}
import { LineSeries, LineStyle, type SeriesDefinition, type SeriesPartialOptionsMap } from 'lightweight-charts';
import { getThemeStore } from '@src/theme/store';
import { SingleValueSeriesStrategy } from './SingleValueSeriesStrategy';
export class LineSeriesStrategy extends SingleValueSeriesStrategy<'Line'> {
protected seriesDefinition(): SeriesDefinition<'Line'> {
return LineSeries;
}
protected getDefaultOptions(): SeriesPartialOptionsMap['Line'] {
const { colors } = getThemeStore();
return {
color: colors.chartLineColor,
lineWidth: 2,
lineStyle: LineStyle.Solid,
};
}
public getTypeName(): string {
return 'Line';
}
}
import { HistogramSeries, type SeriesDefinition, type SeriesPartialOptionsMap } from 'lightweight-charts';
import { SingleValueSeriesStrategy } from './SingleValueSeriesStrategy';
export class HistogramSeriesStrategy extends SingleValueSeriesStrategy<'Histogram'> {
protected seriesDefinition(): SeriesDefinition<'Histogram'> {
return HistogramSeries;
}
protected getDefaultOptions(): SeriesPartialOptionsMap['Histogram'] {
return {};
}
public getTypeName(): string {
return 'Histogram';
}
}
import {
BarData,
CandlestickSeries,
CustomData,
HistogramData,
LineData,
SeriesDataItemTypeMap,
SeriesDefinition,
SeriesPartialOptionsMap,
Time,
} from 'lightweight-charts';
import { Ohlc } from '@core/Legend';
import { BaseSeries, BaseSeriesParams, calcCandleChange } from '@core/Series/BaseSeries';
import { ISeries } from '@src/modules/series-strategies';
import { getThemeStore } from '@src/theme/store';
import { t } from '@src/translations';
import { Candle, LineCandle } from '@src/types';
import { ensureDefined, formatCompactNumber, isBarData } from '@src/utils';
import { removeAlphaFromHex } from '@src/utils/removeAlphaFromHex';
export class CandlestickSeriesStrategy extends BaseSeries<'Candlestick'> implements ISeries<'Candlestick'> {
constructor(params: BaseSeriesParams<'Candlestick'>) {
super(params);
this.subscribeDataSource(params.dataSource);
}
protected seriesDefinition(): SeriesDefinition<'Candlestick'> {
return CandlestickSeries;
}
public getDefaultOptions(): SeriesPartialOptionsMap['Candlestick'] {
const { colors } = getThemeStore();
return {
upColor: colors.chartCandleUp,
downColor: colors.chartCandleDown,
borderVisible: false,
wickUpColor: colors.chartCandleWickUp,
wickDownColor: colors.chartCandleWickDown,
};
}
public validateData(data: (Partial<Candle> & Partial<LineCandle>)[]): boolean {
// todo: should be private
if (!Array.isArray(data)) {
return false;
}
return data.every((point) => {
if (!point) {
return false;
}
// Проверяем обязательные поля
if (typeof point.time !== 'number') {
return false;
}
// Если указаны OHLC, проверяем их корректность
if (
point.open !== undefined &&
point.high !== undefined &&
point.low !== undefined &&
point.close !== undefined
) {
return point.high >= Math.max(point.open, point.close) && point.low <= Math.min(point.open, point.close);
}
return true;
});
}
public getTypeName(): string {
return 'Candlestick';
}
protected dataSourceSubscription = (dataToSet: Candle[]): void => {
if (!this.validateData(dataToSet)) {
console.error(`LightweightAPI: Invalid data format for ${this.getTypeName()} chart`);
return;
}
this.setData(this.formatData(dataToSet));
};
protected dataSourceRealtimeSubscription = (dataToSet: Candle): void => {
if (!this.validateData([dataToSet])) {
console.error(`LightweightAPI: Invalid data format for ${this.getTypeName()} chart`);
return;
}
const [formattedData] = this.formatData([dataToSet]);
if (!formattedData) {
return;
}
this.updateRealtime(formattedData);
};
protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Candlestick'][] {
return inputData.map((point) => ({
time: point.time as Time,
open: point.open,
high: point.high,
low: point.low,
close: point.close,
customValues: point as unknown as Record<string, unknown>,
}));
}
protected formatLegendValues(
currentBar: null | BarData | LineData | HistogramData | CustomData,
prevBar: null | BarData | LineData | HistogramData | CustomData,
): Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>> {
if (!currentBar || !isBarData(currentBar)) {
return {};
}
const { colors } = getThemeStore();
const color = removeAlphaFromHex(
currentBar.close < currentBar.open ? colors.chartCandleWickDown : colors.chartCandleWickUp,
);
const { absoluteChange, percentageChange, time } = ensureDefined(calcCandleChange(prevBar, currentBar));
return {
open: {
value: formatCompactNumber(currentBar.open) ?? '',
name: t('Open'),
color,
},
high: {
value: formatCompactNumber(currentBar.high) ?? '',
name: t('High'),
color,
},
low: {
value: formatCompactNumber(currentBar.low) ?? '',
name: t('Low'),
color,
},
close: {
value: formatCompactNumber(currentBar.close) ?? '',
name: t('Close'),
color,
},
absoluteChange: {
value: formatCompactNumber(absoluteChange ?? 0),
name: t('Change'),
color,
},
percentageChange: {
value: percentageChange !== undefined ? `${formatCompactNumber(percentageChange)}%` : '',
name: t('Change'),
color,
},
time: {
value: time,
name: t('Time'),
color,
},
};
}
}
import {
BarData,
BarSeries,
CustomData,
HistogramData,
LineData,
SeriesDataItemTypeMap,
SeriesDefinition,
SeriesPartialOptionsMap,
Time,
} from 'lightweight-charts';
import { Ohlc } from '@core/Legend';
import { BaseSeries, BaseSeriesParams, calcCandleChange } from '@src/core/Series/BaseSeries';
import { ISeries } from '@src/modules/series-strategies';
import { getThemeStore } from '@src/theme';
import { t } from '@src/translations';
import { Candle, LineCandle } from '@src/types';
import { ensureDefined, formatCompactNumber, isBarData } from '@src/utils';
import { removeAlphaFromHex } from '@src/utils/removeAlphaFromHex';
export class BarSeriesStrategy extends BaseSeries<'Bar'> implements ISeries<'Bar'> {
constructor(params: BaseSeriesParams<'Bar'>) {
super(params);
this.subscribeDataSource(params.dataSource);
}
protected seriesDefinition(): SeriesDefinition<'Bar'> {
return BarSeries;
}
getDefaultOptions(): SeriesPartialOptionsMap['Bar'] {
const { colors } = getThemeStore();
return {
upColor: colors.chartCandleUp,
downColor: colors.chartCandleDown,
};
}
getTypeName(): string {
return 'Bar';
}
public validateData(data: (Partial<Candle> & Partial<LineCandle>)[]): boolean {
if (!Array.isArray(data)) {
return false;
}
return data.every((point) => typeof point.time === 'number');
}
protected dataSourceSubscription = (dataToSet: Candle[]): void => {
if (!this.validateData(dataToSet)) {
console.error(`LightweightAPI: Invalid data format for ${this.getTypeName()} chart`);
return;
}
this.setData(this.formatData(dataToSet));
};
protected dataSourceRealtimeSubscription = (dataToSet: Candle): void => {
if (!this.validateData([dataToSet])) {
console.error(`LightweightAPI: Invalid data format for ${this.getTypeName()} chart`);
return;
}
const [formattedData] = this.formatData([dataToSet]);
if (!formattedData) {
return;
}
this.updateRealtime(formattedData);
};
protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Bar'][] {
return inputData.map((point) => ({
time: point.time as Time,
open: point.open,
high: point.high,
low: point.low,
close: point.close,
customValues: point as unknown as Record<string, unknown>,
}));
}
protected formatLegendValues(
currentBar: null | BarData | LineData | HistogramData | CustomData,
prevBar: null | BarData | LineData | HistogramData | CustomData,
): Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>> {
if (!currentBar || !isBarData(currentBar)) {
return {};
}
const { colors } = getThemeStore();
const color = removeAlphaFromHex(
currentBar.close < currentBar.open ? colors.chartCandleWickDown : colors.chartCandleWickUp,
);
const { absoluteChange, percentageChange, time } = ensureDefined(calcCandleChange(prevBar, currentBar));
return {
open: {
value: formatCompactNumber(currentBar.open) ?? '',
name: t('Open'),
color,
},
high: {
value: formatCompactNumber(currentBar.high) ?? '',
name: t('High'),
color,
},
low: {
value: formatCompactNumber(currentBar.low) ?? '',
name: t('Low'),
color,
},
close: {
value: formatCompactNumber(currentBar.close) ?? '',
name: t('Close'),
color,
},
absoluteChange: {
value: formatCompactNumber(absoluteChange ?? 0),
name: t('Change'),
color,
},
percentageChange: {
value: percentageChange !== undefined ? `${formatCompactNumber(percentageChange)}%` : '',
name: t('Change'),
color,
},
time: {
value: time,
name: t('Time'),
color,
},
};
}
}