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


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..3775322d428c20fa1ad69d4ef349dff4ba2242ca
--- /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 sumDiviation({
+                    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: 20 },
+      { time: 3, value: 36 },
+      { time: 4, value: 64 },
+      { time: 5, value: 100.6 },
+      { time: 6, value: 150.01 }
+    ]);
+
+    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: 150.01 } ]);
+
+    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: 150.01 } ]);
+  }); 
+  
+});
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..3fc76ad0577499dc40da1147964089c5e6b86af7
--- /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 prevVWAPTime = vwapBar.time as number
+
+    const isSameDay = dayjs(prevVWAPTime*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 {