Загрузка данных
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a27153d781625fb8b87dcd91bde52149efa0f954..f3427352bb644817b651aa1fe5afaf71cc564f8d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
- Добавлена возможность копирования изображения графика в буфер обмена
- Добавлены тултипы для элементов управления в шапке графика
- Добавлен параметр `showSnapshotButton?: boolean` для управления отображением кнопки «Снимок графика»
+- Добавлен VWAP индикатор
# 0.2.1
diff --git a/src/constants/indicator.ts b/src/constants/indicator.ts
index aac36aa7b01fd602d3f302387686599f7b3881f1..de1f75351b518ea3ffd0146a9bd1417fd9d81c6c 100644
--- a/src/constants/indicator.ts
+++ b/src/constants/indicator.ts
@@ -6,6 +6,7 @@ export enum IndicatorsIds { // todo: rename to IndicatorsTypes
EMA = 'ema',
MACD = 'macd',
RSI = 'RSI',
+ VWAP = 'VWAP',
}
export const indicatorSeriesLabelById: Partial<Record<IndicatorsIds, Record<string, string>>> = {
@@ -24,4 +25,5 @@ export const indicatorLabelById = () => ({
[IndicatorsIds.EMA]: 'EMA',
[IndicatorsIds.MACD]: 'MACD',
[IndicatorsIds.RSI]: 'RSI',
+ [IndicatorsIds.VWAP]: 'VWAP',
});
diff --git a/src/core/Indicators/__tests__/vwap.test.ts b/src/core/Indicators/__tests__/vwap.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..de3edfea9c735e981d3562bcaa8a72346e045f42
--- /dev/null
+++ b/src/core/Indicators/__tests__/vwap.test.ts
@@ -0,0 +1,500 @@
+import { Coordinate, Time } from "lightweight-charts";
+import { cumulativePriceVolume, cumulativeVolume, standartDiviation, sumDiviation, VWAP, VWAPBounds } from "../vwap";
+import { SeriesApi } from "@src/core/Drawings/types";
+import { ChartTypeToCandleData, IndicatorDataFormatter } from "..";
+import { SerieData } from "@src/core/Series/BaseSeries";
+import { Indicator } from "@src/core/Indicator";
+import { SeriesStrategies } from "@src/modules/series-strategies/SeriesFactory";
+
+
+const mainSerieData: SerieData[] = [
+ {
+ time: 1 as Time,
+ customValues: {
+ time: 1,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 5,
+ }
+ },
+ {
+ time: 2 as Time,
+ customValues: {
+ time: 2,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 10,
+ }
+ },
+ {
+ time: 3 as Time,
+ customValues: {
+ time: 3,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 15,
+ }
+ },
+ {
+ time: 4 as Time,
+ customValues: {
+ time: 4,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 20,
+ }
+ },
+ {
+ time: 5 as Time,
+ customValues: {
+ time: 5,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 25,
+ }
+ },
+ {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+]
+
+const indicator = {
+ getSeriesMap: () => {
+ const cumulativePriceVolumeStratagy: Partial<SeriesStrategies> = {
+ data: () => {
+ return cumulativePriceVolume({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ })
+ }
+ }
+ const cumulativeVolumeStratagy: Partial<SeriesStrategies> = {
+ data: () => {
+ return cumulativeVolume({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ })
+
+ }
+ }
+ const VWAPStratagy: Partial<SeriesStrategies> = {
+ data: () => {
+ return VWAP({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ indicatorReference: indicator as Indicator
+ })
+ }
+ }
+ const sumDiviationStratagy: Partial<SeriesStrategies> = {
+ data: () => {
+ return sumDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ indicatorReference: indicator as Indicator
+ })
+ }
+ }
+ const standartDiviationStratagy: Partial<SeriesStrategies> = {
+ data: () => {
+ return standartDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ indicatorReference: indicator as Indicator
+ })
+ }
+ }
+ return new Map<string, Partial<SeriesStrategies>>([['cumulativePriceVolume', cumulativePriceVolumeStratagy], ['cumulativeVolume', cumulativeVolumeStratagy], ['VWAP', VWAPStratagy], ['sumDiviation', sumDiviationStratagy], ['standartDiviation', standartDiviationStratagy]])
+ }
+}
+
+
+describe('WVAP calculation', () => {
+ it('cumulativeVolume function', () => {
+ const selfData = cumulativeVolume({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ })
+
+ expect(selfData).toMatchObject([
+ { time: 1, value: 5 },
+ { time: 2, value: 15 },
+ { time: 3, value: 30 },
+ { time: 4, value: 50 },
+ { time: 5, value: 75 },
+ { time: 6, value: 105 }
+ ]);
+
+ expect(cumulativeVolume({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 7 as Time,
+ customValues: {
+ time: 7,
+ open: 5,
+ high: 9,
+ low: 1,
+ close: 7,
+ volume: 35,
+ }
+ },
+ })).toMatchObject([ { time: 7, value: 140 } ]);
+
+ expect(cumulativeVolume({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ candle: {
+ time: 7 as Time,
+ customValues: {
+ time: 7,
+ open: 5,
+ high: 9,
+ low: 1,
+ close: 7,
+ volume: 35,
+ }
+ },
+ })).toMatchObject([ { time: 7, value: 35 } ]);
+ });
+
+ it('cumulativePriceVolume function', () => {
+ const selfData = cumulativePriceVolume({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ })
+
+ expect(selfData).toMatchObject([
+ { time: 1, value: 60 },
+ { time: 2, value: 150 },
+ { time: 3, value: 330 },
+ { time: 4, value: 510 },
+ { time: 5, value: 810 },
+ { time: 6, value: 1080 }
+ ]);
+
+ expect(cumulativePriceVolume({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 7 as Time,
+ customValues: {
+ time: 7,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 35,
+ }
+ },
+ })).toMatchObject([ { time: 7, value: 1500 } ]);
+
+ expect(cumulativePriceVolume({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ candle: {
+ time: 7 as Time,
+ customValues: {
+ time: 7,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 35,
+ }
+ },
+ })).toMatchObject([ { time: 7, value: 420 } ]);
+ });
+
+ it('VWAP function', () => {
+ const selfData = VWAP({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ indicatorReference: indicator as Indicator
+ })
+
+ expect(selfData).toMatchObject([
+ { time: 1, value: 12 },
+ { time: 2, value: 10 },
+ { time: 3, value: 11 },
+ { time: 4, value: 10.2 },
+ { time: 5, value: 10.8 },
+ { time: 6, value: 10.29 }
+ ]);
+
+ expect(VWAP({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 7 as Time,
+ customValues: {
+ time: 7,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 35,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 7 } ]);
+
+ expect(VWAP({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 6, value: 10.29 } ]);
+
+ expect(VWAP({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ candle: {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 6, value: 10.29 } ]);
+ });
+
+ it('sumDiviation function', () => {
+ const selfData = sumDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ indicatorReference: indicator as Indicator
+ })
+
+ expect(selfData).toMatchObject([
+ { time: 1, value: 0 },
+ { time: 2, value: 10 },
+ { time: 3, value: 25 },
+ { time: 4, value: 53.8 },
+ { time: 5, value: 89.8 },
+ { time: 6, value: 139.72 }
+ ]);
+
+ expect(sumDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 7 as Time,
+ customValues: {
+ time: 7,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 35,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 7 } ]);
+
+ expect(sumDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 6, value: 139.72 } ]);
+
+ expect(sumDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ candle: {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 6, value: 49.92 } ]);
+ });
+
+ it('standartDiviation function', () => {
+ const selfData = standartDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ indicatorReference: indicator as Indicator
+ })
+
+ expect(selfData).toMatchObject([
+ { time: 1, value: 0 },
+ { time: 2, value: 0.82 },
+ { time: 3, value: 0.91 },
+ { time: 4, value: 1.04 },
+ { time: 5, value: 1.09 },
+ { time: 6, value: 1.15 }
+ ]);
+
+ expect(standartDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 7 as Time,
+ customValues: {
+ time: 7,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 35,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 7 } ]);
+
+ expect(standartDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 6, value: 1.15 } ]);
+
+ expect(standartDiviation({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ candle: {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ })).toMatchObject([ { time: 6, value: 1.15 } ]);
+
+ });
+
+ it('VWAPBounds function', () => {
+ const selfData = VWAPBounds({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ indicatorReference: indicator as Indicator
+ }, true)
+
+ expect(selfData).toMatchObject([
+ { time: 1, value: 12 },
+ { time: 2, value: 10.82 },
+ { time: 3, value: 11.91 },
+ { time: 4, value: 11.24 },
+ { time: 5, value: 11.89 },
+ { time: 6, value: 11.44 }
+ ]);
+
+ expect(VWAPBounds({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 7 as Time,
+ customValues: {
+ time: 7,
+ open: 10,
+ high: 20,
+ low: 1,
+ close: 15,
+ volume: 35,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ }, true)).toMatchObject([ { time: 7 } ]);
+
+ expect(VWAPBounds({
+ mainSeriesData: mainSerieData,
+ selfData: selfData as ChartTypeToCandleData['Line'][],
+ candle: {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ }, true)).toMatchObject([ { time: 6, value: 11.44 } ]);
+
+ expect(VWAPBounds({
+ mainSeriesData: mainSerieData,
+ selfData: [],
+ candle: {
+ time: 6 as Time,
+ customValues: {
+ time: 6,
+ open: 15,
+ high: 15,
+ low: 2,
+ close: 10,
+ volume: 30,
+ }
+ },
+ indicatorReference: indicator as Indicator
+ }, true)).toMatchObject([ { time: 6, value: 11.44 } ]);
+ });
+
+});
diff --git a/src/core/Indicators/index.tsx b/src/core/Indicators/index.tsx
index 753b2ae384b84c58658da16fa8e4893e66be2ee5..bb6409bae4a8f6a9c1eb31efcae8caaf48d56378 100644
--- a/src/core/Indicators/index.tsx
+++ b/src/core/Indicators/index.tsx
@@ -14,6 +14,7 @@ import { t } from '@src/translations';
import { IndicatorConfig, LineCandle, SettingsValues } from '@src/types';
import { gains, gainsSma, rs, rsi, rsiMa } from './rsi';
+import { cumulativePriceVolume, cumulativeVolume, standartDiviation, sumDiviation, VWAP, VWAPBounds } from './vwap';
export type ChartTypeToCandleData = {
['Bar']: Candle;
@@ -325,4 +326,103 @@ export const indicatorsMap = (): Partial<Record<IndicatorsIds, IndicatorConfig>>
],
settings: [{ type: 'number', key: 'MA_Length', label: t('MA length'), defaultValue: 14, min: 5, max: 500 }],
},
+
+ [IndicatorsIds.VWAP]: {
+ newPane: false,
+ series: [
+ {
+ name: 'Line',
+ actLikeMainSerie: true,
+ id: 'cumulativePriceVolume',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => cumulativePriceVolume(params as IndicatorDataFormatter<'Line'>),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: true,
+ },
+ },
+ {
+ name: 'Line',
+ actLikeMainSerie: true,
+ id: 'cumulativeVolume',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => cumulativeVolume(params as IndicatorDataFormatter<'Line'>),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: true,
+ },
+ },
+ {
+ name: 'Line',
+ actLikeMainSerie: true,
+ id: 'VWAP',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => VWAP(params as IndicatorDataFormatter<'Line'>),
+ seriesOptions: {
+ visible: true,
+ lastValueVisible: true,
+ },
+ },
+ {
+ name: 'Line',
+ actLikeMainSerie: true,
+ id: 'sumDiviation',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => sumDiviation(params as IndicatorDataFormatter<'Line'>),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: true,
+ },
+ },
+ {
+ name: 'Line',
+ actLikeMainSerie: true,
+ id: 'standartDiviation',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => standartDiviation(params as IndicatorDataFormatter<'Line'>),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: true,
+ },
+ },
+ {
+ name: 'Line',
+ actLikeMainSerie: true,
+ id: 'VWAPTopBound',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => VWAPBounds(params as IndicatorDataFormatter<'Line'>, true),
+ seriesOptions: {
+ visible: true,
+ lastValueVisible: true,
+ color: '#00e676'
+ },
+ },
+ {
+ name: 'Line', // todo: change with enum
+ actLikeMainSerie: true,
+ id: 'VWAPBottomBound',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => VWAPBounds(params as IndicatorDataFormatter<'Line'>, false),
+ seriesOptions: {
+ visible: true,
+ lastValueVisible: true,
+ color: '#00e676'
+ },
+ },
+ ],
+ },
});
diff --git a/src/core/Indicators/rsi.ts b/src/core/Indicators/rsi.ts
index bcac80088f43ea321925e456ce447447bab407d6..5a2c91baf4bdda54b93e5a6bf33191b9f691014c 100644
--- a/src/core/Indicators/rsi.ts
+++ b/src/core/Indicators/rsi.ts
@@ -1,4 +1,4 @@
-import { SeriesDataItemTypeMap, Time, WhitespaceData } from 'lightweight-charts';
+import { SeriesDataItemTypeMap, Time } from 'lightweight-charts';
import { LineCandle } from '@src/types';
import { isLineData } from '@src/utils';
diff --git a/src/core/Indicators/vwap.ts b/src/core/Indicators/vwap.ts
new file mode 100644
index 0000000000000000000000000000000000000000..faee5c5c0cafedf558ba5d0f524f1fc475012a83
--- /dev/null
+++ b/src/core/Indicators/vwap.ts
@@ -0,0 +1,560 @@
+import { SeriesDataItemTypeMap, Time } from "lightweight-charts";
+import { IndicatorDataFormatter } from ".";
+import dayjs from "dayjs";
+import { isLineData } from "@src/utils";
+import { SerieData } from "../Series/BaseSeries";
+
+export function VWAPBounds({
+ mainSeriesData,
+ selfData,
+ candle,
+ indicatorReference,
+ settings
+}: IndicatorDataFormatter<'Line'>, isTop: boolean): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!indicatorReference || !mainSeriesData) return [];
+
+
+ const standartDiviationLine = (indicatorReference.getSeriesMap().get('standartDiviation')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+ const vwapLine = (indicatorReference.getSeriesMap().get('VWAP')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+
+
+ if (!candle) {
+ return VWAPBoundsBatch({mainSeriesData, vwapLine, standartDiviationLine, isTop})
+ }
+ const standartDiviationBar = standartDiviationLine.at(-1)?.time === candle.time ? standartDiviationLine.at(-1) : undefined;
+ const vwapBar = vwapLine.at(-1)?.time === candle.time ? vwapLine.at(-1) : undefined;
+
+ if (
+ !standartDiviationBar ||
+ !isLineData(standartDiviationBar) ||
+ !vwapBar ||
+ !isLineData(vwapBar) ||
+ standartDiviationBar.time !== candle.time ||
+ vwapBar.time !== candle.time
+ ) {
+ return [{ time: candle.time }];
+ }
+
+ const direction = isTop ? 1 : -1
+
+ return [{
+ time: candle.time,
+ value: +(vwapBar.value + direction * standartDiviationBar.value).toFixed(2)
+ }]
+}
+
+function VWAPBoundsBatch({
+ mainSeriesData,
+ vwapLine,
+ standartDiviationLine,
+ isTop,
+}: {
+ mainSeriesData: SerieData[],
+ vwapLine: SeriesDataItemTypeMap['Line'][],
+ standartDiviationLine: SeriesDataItemTypeMap['Line'][],
+ isTop: boolean,
+}) {
+ const direction = isTop ? 1 : -1
+
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = mainSeriesData.map((lineCandle) => ({
+ time: lineCandle.time,
+ }));
+
+ for (let i = 0; i < mainSeriesData.length; i++) {
+ const standartDiviationBar = standartDiviationLine[i];
+ const vwapBar = vwapLine[i];
+
+ if (!isLineData(standartDiviationBar) ||!isLineData(vwapBar)) {
+ throw new Error('[VWAP] can not build indicator VWAP line')
+ }
+
+ res[i] = {
+ time: vwapBar.time,
+ value: +(vwapBar.value + direction * standartDiviationBar.value).toFixed(2)
+ }
+ }
+ return res
+}
+
+
+export function standartDiviation({
+ mainSeriesData,
+ selfData,
+ candle,
+ indicatorReference,
+ settings
+}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!indicatorReference || !mainSeriesData) return [];
+
+ const sumDiviationLine = (indicatorReference.getSeriesMap().get('sumDiviation')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+ const cumulativeVolumeLine = (indicatorReference.getSeriesMap().get('cumulativeVolume')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = mainSeriesData.map((lineCandle) => ({
+ time: lineCandle.time,
+ }));
+
+
+ if (!candle) {
+ mainSeriesData.forEach((bar, index, self) => {
+ const sumDiviationBar = sumDiviationLine[index]
+ const cumulativeVolumeBar = cumulativeVolumeLine[index]
+
+ if(!isLineData(sumDiviationBar) || !isLineData(cumulativeVolumeBar)){
+ throw new Error('[VWAP indicator] error occured while calculation of the diviation')
+ }
+
+ res[index] = {
+ time: bar.time,
+ value: +Math.sqrt(sumDiviationBar.value / cumulativeVolumeBar.value).toFixed(2)
+ }
+ });
+
+ return res
+ }
+
+ const sumDiviationBar = sumDiviationLine.at(-1)?.time === candle.time ? sumDiviationLine.at(-1) : undefined;
+ const cumulativeVolumeBar = cumulativeVolumeLine.at(-1)?.time === candle.time ? cumulativeVolumeLine.at(-1) : undefined;
+
+ if (
+ !sumDiviationBar ||
+ !isLineData(sumDiviationBar) ||
+ !cumulativeVolumeBar ||
+ !isLineData(cumulativeVolumeBar) ||
+ sumDiviationBar.time !== candle.time ||
+ cumulativeVolumeBar.time !== candle.time
+ ) {
+ return [{ time: candle.time }];
+ }
+ return [{
+ time: candle.time,
+ value: +Math.sqrt(sumDiviationBar.value / cumulativeVolumeBar.value).toFixed(2)
+ }]
+
+}
+
+export function sumDiviation({
+ mainSeriesData,
+ selfData,
+ candle,
+ indicatorReference,
+ settings
+}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!indicatorReference || !mainSeriesData) return [];
+
+ const vwapLine = (indicatorReference.getSeriesMap().get('VWAP')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = mainSeriesData.map((lineCandle) => ({
+ time: lineCandle.time,
+ }));
+
+ if (!candle) {
+ return sumDiviationBatch({mainSeriesData, vwapLine, res})
+ }
+
+ const vwapBar = vwapLine.at(-1)?.time === candle.time ? vwapLine.at(-1) : undefined;
+
+ if (
+ !vwapBar ||
+ !isLineData(vwapBar) ||
+ vwapBar.time !== candle.time
+ ) {
+ return [{ time: candle.time }];
+ }
+
+ const prevSelfData = selfData.at(-1)?.time === candle.time
+ ? selfData.at(-2)
+ : selfData.at(-1)
+
+ if(!candle.customValues.volume){
+ throw new Error('[VWAP indicator] unable to calculate indicator for serie data without volume')
+ }
+
+ const typicalPrice: number = (candle.customValues.close + candle.customValues.high + candle.customValues.low) / 3;
+
+ const currentDeviation = candle.customValues.volume * Math.pow((typicalPrice - vwapBar.value), 2)
+
+ if(!prevSelfData){
+ return [{
+ time: candle.time,
+ value: +currentDeviation.toFixed(2)
+ }]
+ }
+
+ const prevSelfDataTime = prevSelfData.time
+
+ const isSameDay = dayjs(prevSelfDataTime*1000).isSame(<number>candle.time*1000, 'day')
+
+ const shouldDropAccomulation = !isSameDay
+
+ if(shouldDropAccomulation){
+ return [{
+ time: candle.time,
+ value: +currentDeviation.toFixed(2)
+ }]
+ }
+
+ return [{
+ time: candle.time,
+ value: +((prevSelfData.value ?? 0) + currentDeviation).toFixed(2)
+ }]
+}
+
+function sumDiviationBatch({mainSeriesData, vwapLine, res}: {mainSeriesData: SerieData[], vwapLine: SeriesDataItemTypeMap['Line'][], res: SeriesDataItemTypeMap<Time>['Line'][]}) {
+ for (let i = 0; i < mainSeriesData.length; i++) {
+ const vwapBar = vwapLine[i];
+ const prevCandle = vwapLine[i - 1]
+
+ if (!isLineData(vwapBar)) {
+ throw new Error('[VWAP] can not build indicator VWAP line')
+ }
+ const bar = mainSeriesData[i].customValues
+
+ const typicalPrice: number = (bar.close + bar.high + bar.low) / 3;
+
+ if(!bar.volume){
+ throw new Error('[VWAP indicator] unable to calculate indicator for serie data without volume')
+ }
+
+ const currentDeviation = bar.volume * Math.pow((typicalPrice - vwapBar.value), 2)
+
+ if(!prevCandle){
+ res[i] = {
+ time: vwapBar.time,
+ value: currentDeviation
+ }
+
+ continue
+ }
+
+ const prevCandleTime = (prevCandle.time as number)
+
+ const isSameDay = dayjs(prevCandleTime*1000).isSame(bar.time*1000, 'day')
+
+ const shouldDropAccomulation = !isSameDay
+
+ if(shouldDropAccomulation){
+ // drop accoumulation
+ res[i] = {
+ time: vwapBar.time,
+ value: currentDeviation
+ }
+
+ continue
+ }
+
+ const prevSumDiviationBar = res[i - 1]
+
+ if(!isLineData(prevSumDiviationBar)) {
+ throw new Error('[VWAP indicator] unable to calculate indicator for serie data without volume')
+ }
+
+ res[i] = {
+ time: vwapBar.time,
+ value: prevSumDiviationBar.value + currentDeviation
+ }
+ }
+
+ return res.map((bar) => {
+ if (isLineData(bar)) {
+ return {
+ ...bar,
+ value: +bar.value.toFixed(2),
+ };
+ }
+ return bar;
+ });
+}
+
+export function VWAP({
+ mainSeriesData,
+ selfData,
+ candle,
+ indicatorReference,
+ settings
+}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!indicatorReference) return [];
+ if (!mainSeriesData) return [];
+
+ const cumulativePriceVolumeLine = (indicatorReference.getSeriesMap().get('cumulativePriceVolume')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+ const cumulativeVolumeLine = (indicatorReference.getSeriesMap().get('cumulativeVolume')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+
+ if (!candle) {
+ return VWAPBatch({mainSeriesData, cumulativePriceVolumeLine, cumulativeVolumeLine})
+ }
+
+ const priceVolumeBar = cumulativePriceVolumeLine.at(-1)?.time === candle.time ? cumulativePriceVolumeLine.at(-1) : undefined;
+ const volumeBar = cumulativeVolumeLine.at(-1)?.time === candle.time ? cumulativeVolumeLine.at(-1) : undefined;
+
+ if (
+ !priceVolumeBar ||
+ !isLineData(priceVolumeBar) ||
+ !volumeBar ||
+ !isLineData(volumeBar) ||
+ priceVolumeBar.time !== candle.time ||
+ volumeBar.time !== candle.time
+ ) {
+ return [{ time: candle.time }];
+ }
+
+ return [{
+ time: priceVolumeBar.time as Time,
+ value: +(priceVolumeBar.value / volumeBar.value).toFixed(2)
+ }]
+}
+
+function VWAPBatch({
+ mainSeriesData,
+ cumulativePriceVolumeLine,
+ cumulativeVolumeLine,
+}: {
+ mainSeriesData: SerieData[],
+ cumulativePriceVolumeLine: SeriesDataItemTypeMap['Line'][],
+ cumulativeVolumeLine: SeriesDataItemTypeMap['Line'][],
+}) {
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = mainSeriesData.map((lineCandle) => ({
+ time: lineCandle.time,
+ }));
+
+ for (let i = 0; i < mainSeriesData.length; i++) {
+ const priceVolumeBar = cumulativePriceVolumeLine[i];
+ const volumeBar = cumulativeVolumeLine[i];
+
+ if (!isLineData(priceVolumeBar) || !isLineData(volumeBar)) {
+ throw new Error('[VWAP] can not build indicator VWAP line')
+ }
+
+ res[i] = {
+ time: priceVolumeBar.time as Time,
+ value: priceVolumeBar.value / volumeBar.value
+ }
+ }
+
+ return res.map((bar) => {
+ if (isLineData(bar)) {
+ return {
+ ...bar,
+ value: +bar.value.toFixed(2),
+ };
+ }
+ return bar;
+ });
+}
+
+export function cumulativePriceVolume({
+ mainSeriesData,
+ selfData,
+ candle,
+ indicatorReference,
+ settings
+}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!mainSeriesData) return [];
+
+ if (!candle) {
+ return cumulativePriceVolumeBatch(mainSeriesData)
+ }
+
+ const prevSelfData = selfData.at(-1)?.time === candle.time
+ ? selfData.at(-2)
+ : selfData.at(-1)
+
+ if(!candle.customValues.volume){
+ throw new Error('[VWAP indicator] unable to calculate indicator for serie data without volume')
+ }
+
+ const typicalPrice: number = (candle.customValues.close + candle.customValues.high + candle.customValues.low) / 3;
+
+ if(!prevSelfData){
+ return [{
+ time: candle.time,
+ value: typicalPrice * candle.customValues.volume,
+ }]
+ }
+
+ const prevSelfTime = prevSelfData.time
+
+ const isSameDay = dayjs(prevSelfTime*1000).isSame(<number>candle.time*1000, 'day')
+
+ const shouldDropAccomulation = !isSameDay
+
+ if(shouldDropAccomulation){
+ // drop accoumulation
+ return [{
+ time: candle.time,
+ value: typicalPrice * candle.customValues.volume
+ }]
+ }
+
+ const tradeCost = (prevSelfData?.value ?? 0) + typicalPrice * candle.customValues.volume
+
+ return [{
+ time: candle.time,
+ value: tradeCost
+ }]
+}
+
+function cumulativePriceVolumeBatch(mainSeriesData: SerieData[]){
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = mainSeriesData.map((lineCandle) => ({ time: lineCandle.time }));
+
+ mainSeriesData.forEach((bar, index, self) => {
+ const prevCandle = self[index - 1]
+
+ if(!prevCandle){
+ const typicalPrice: number = (bar.customValues.close + bar.customValues.high + bar.customValues.low) / 3;
+
+ if(!bar.customValues.volume){
+ throw new Error('[VWAP indicator] unable to calculate indicator for serie data without volume')
+ }
+
+ res[index] = {
+ time: bar.time,
+ value: typicalPrice * bar.customValues.volume
+ }
+
+ return
+ }
+
+ const prevCandleTime = <number>prevCandle.time
+
+ const isSameDay = dayjs(prevCandleTime*1000).isSame(<number>bar.time*1000, 'day')
+
+ const shouldDropAccomulation = !isSameDay
+
+ if(!bar.customValues.volume){
+ throw new Error('[VWAP indicator] unable to calculate indicator for serie data without volume')
+ }
+
+ if(shouldDropAccomulation){
+ // drop accoumulation
+ const typicalPrice: number = (bar.customValues.close + bar.customValues.high + bar.customValues.low) / 3;
+
+ res[index] = {
+ time: bar.time,
+ value: typicalPrice * bar.customValues.volume
+ }
+
+ return
+ }
+
+ const typicalPrice: number = (bar.customValues.close + bar.customValues.high + bar.customValues.low) / 3;
+
+ const prevPriceVolumeBar = res[index - 1]
+ if(!isLineData(prevPriceVolumeBar)){
+ throw new Error('[VWAP indicator] error occured while calculation of the price volume comulative')
+ }
+
+ const tradeCost = prevPriceVolumeBar.value + typicalPrice * bar.customValues.volume
+
+ res[index] = {
+ time: bar.time,
+ value: tradeCost
+ }
+ });
+
+ return res;
+
+}
+
+export function cumulativeVolume({
+ mainSeriesData,
+ selfData,
+ candle,
+ indicatorReference,
+ settings
+}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!mainSeriesData) return [];
+
+ if (!candle) {
+ return cumulativeVolumeBatch(mainSeriesData)
+ }
+
+ const prevSelfData = selfData.at(-1)?.time === candle.time
+ ? selfData.at(-2)
+ : selfData.at(-1)
+
+ if(!prevSelfData){
+ return [{
+ time: candle.time,
+ value: candle.customValues.volume
+ }]
+ }
+
+ if(!candle.customValues.volume){
+ throw new Error('[VWAP indicator] unable to calculate indicator for serie data without volume')
+ }
+
+ const prevSelfTime = prevSelfData.time
+
+ const isSameDay = dayjs(prevSelfTime*1000).isSame(<number>candle.time*1000, 'day')
+
+ const shouldDropAccomulation = !isSameDay
+
+ if(shouldDropAccomulation){
+ // drop accoumulation
+ return [{
+ time: candle.time,
+ value: candle.customValues.volume
+ }]
+ }
+
+ return [{
+ time: candle.time,
+ value: (prevSelfData?.value ?? 0) + candle.customValues.volume
+ }]
+
+}
+
+function cumulativeVolumeBatch(mainSeriesData: SerieData[]) {
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = mainSeriesData.map((lineCandle) => ({ time: lineCandle.time }));
+
+ mainSeriesData.forEach((bar, index, self) => {
+ const prevCandle = self[index - 1]
+
+ if(!bar.customValues.volume){
+ throw new Error('[VWAP indicator] unable to calculate indicator for serie data without volume')
+ }
+
+ if(!prevCandle){
+ res[index] = {
+ time: bar.time,
+ value: bar.customValues.volume
+ }
+
+ return
+ }
+
+ const prevCandleTime = prevCandle.time as number
+
+ const isSameDay = dayjs(prevCandleTime*1000).isSame(<number>bar.time*1000, 'day')
+
+ const shouldDropAccomulation = !isSameDay
+
+ if(shouldDropAccomulation){
+ // drop accoumulation
+ res[index] = {
+ time: bar.time,
+ value: bar.customValues.volume
+ }
+
+ return
+ }
+
+ const prevVolumeBar = res[index - 1]
+ if(!isLineData(prevVolumeBar)){
+ throw new Error('[VWAP indicator] error occured while calculation of the cumulative volume')
+ }
+
+ res[index] = {
+ time: bar.time,
+ value: (prevVolumeBar.value ?? 0) + bar.customValues.volume
+ }
+ });
+
+ return res;
+}
diff --git a/src/utils/indicator.ts b/src/utils/indicator.ts
index 1c9c4dacac855293dcf02439c812b99ca65fe338..ea485280f57ec2146213bbac14c1369a8ddbf465 100644
--- a/src/utils/indicator.ts
+++ b/src/utils/indicator.ts
@@ -102,7 +102,7 @@ export function createFallbackColor(index: number): string {
function shouldUsePaletteColor(serie: IndicatorSerie): boolean {
const options = serie.seriesOptions as SerieOptionsWithColor | undefined;
- return serie.name === 'Line' && options?.visible !== false;
+ return serie.name === 'Line' && options?.visible !== false && !options?.color;
}
function getSerieColor(serie: IndicatorSerie): string | null {
diff --git a/src/utils/typeGuards.ts b/src/utils/typeGuards.ts
index a95c4ea082fece10d33c16d507c9c1deecc0e9fd..66f59d8661b9b7295f9b81934d4ceba40e9785a4 100644
--- a/src/utils/typeGuards.ts
+++ b/src/utils/typeGuards.ts
@@ -1,18 +1,18 @@
import { BarData, CustomData, HistogramData, LineData } from 'lightweight-charts';
-export function isBarData(value: BarData | LineData | HistogramData | CustomData): value is BarData {
- return (
- (value as BarData).open !== undefined &&
- (value as BarData).high !== undefined &&
- (value as BarData).low !== undefined &&
- (value as BarData).close !== undefined
- );
+export function isBarData(value: unknown): value is BarData {
+ return value !== undefined
+ && value !== null
+ && Object.hasOwn(value, 'open')
+ && Object.hasOwn(value, 'high')
+ && Object.hasOwn(value, 'low')
+ && Object.hasOwn(value, 'close')
}
-export function isLineData(value: BarData | LineData | HistogramData | CustomData): value is LineData {
- return (value as LineData | HistogramData).value !== undefined;
+export function isLineData(value: unknown): value is LineData {
+ return value !== undefined && value !== null && Object.hasOwn(value, 'value')
}
-export function isHistogramData(value: BarData | LineData | HistogramData | CustomData): value is HistogramData {
- return (value as LineData | HistogramData).value !== undefined;
+export function isHistogramData(value: unknown): value is HistogramData {
+ return value !== undefined && value !== null && Object.hasOwn(value, 'value')
}