Загрузка данных
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf1ff796554908057b3128cb16a513bf580ec23f..3650fd7b2837d684f142f5e2fc3f709d94463e9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# latest
+- Добавлен индикатор RSI
+- Исправлен баг, когда линия EMA уходила в будущее
+
+# 0.1.21
+
- Добавлена возможность добавлять инструменты сравнения на абсолютную шкалу
- Исправлена работа режима «Продолжить в режиме рисования» для линейки
- Добавлен быстрый запуск линейки с помощью Shift
diff --git a/src/constants/indicator.ts b/src/constants/indicator.ts
index 323eca43079134f2343721dbd60352345661e979..aac36aa7b01fd602d3f302387686599f7b3881f1 100644
--- a/src/constants/indicator.ts
+++ b/src/constants/indicator.ts
@@ -5,6 +5,7 @@ export enum IndicatorsIds { // todo: rename to IndicatorsTypes
SMA = 'sma',
EMA = 'ema',
MACD = 'macd',
+ RSI = 'RSI',
}
export const indicatorSeriesLabelById: Partial<Record<IndicatorsIds, Record<string, string>>> = {
@@ -22,4 +23,5 @@ export const indicatorLabelById = () => ({
[IndicatorsIds.SMA]: 'SMA',
[IndicatorsIds.EMA]: 'EMA',
[IndicatorsIds.MACD]: 'MACD',
+ [IndicatorsIds.RSI]: 'RSI',
});
diff --git a/src/core/Indicator.ts b/src/core/Indicator.ts
index f4faa7227620cf30c4fc239c2cdac4ab2c89a7df..d17870ca1caff9c3b3b3de7e715a235b344f3984 100644
--- a/src/core/Indicator.ts
+++ b/src/core/Indicator.ts
@@ -215,7 +215,7 @@ export class Indicator extends DOMObject implements ISerializable<IndicatorSnaps
private createSeries(): void {
this.config.series.forEach(
- ({ name, id: serieId, dataFormatter, seriesOptions, priceScaleOptions, actLikeMainSerie }) => {
+ ({ name, id: serieId, dataFormatter, seriesOptions, priceScaleOptions, actLikeMainSerie, onSerieInit }) => {
const serie = SeriesFactory.create(name!)({
lwcChart: this.lwcChart,
dataSource: this.dataSource,
@@ -238,6 +238,8 @@ export class Indicator extends DOMObject implements ISerializable<IndicatorSnaps
actLikeMainSerie,
});
+ onSerieInit?.(serie);
+
const handleDataChanged = () => {
this.notifyDataChanged();
};
diff --git a/src/core/Indicators/ema.ts b/src/core/Indicators/ema.ts
index 1904aed0e7fc8d359e374e6f14fa7b7927b35b11..af22e68404c644e483bf0bb33e3f69d96865c3fa 100644
--- a/src/core/Indicators/ema.ts
+++ b/src/core/Indicators/ema.ts
@@ -83,10 +83,8 @@ export function calculatePreciseEMASeriesData(
const candleIndexToCalculate = candleData.findIndex((c) => c.time === candle.time);
if (candleIndexToCalculate === -1) {
- console.error('[Indicators]: нет подходящей свечи в массиве');
return {
time: candle.time as Time,
- value: 0,
};
}
@@ -109,7 +107,6 @@ export function calculateEMASeriesData(
candleData: LineCandle[],
maLength: number,
): SeriesDataItemTypeMap<Time>['Line'][] {
- // todo: change signature to {time & value}
const maData: LineData<Time>[] = [];
const smoothing = 2;
diff --git a/src/core/Indicators/index.tsx b/src/core/Indicators/index.tsx
index d7eb614daccc17c9f78af4ce67abba3bb6990856..753b2ae384b84c58658da16fa8e4893e66be2ee5 100644
--- a/src/core/Indicators/index.tsx
+++ b/src/core/Indicators/index.tsx
@@ -13,6 +13,8 @@ import { INDICATOR_COLOR_PALETTE_MIDDLE_INDEX } from '@src/theme';
import { t } from '@src/translations';
import { IndicatorConfig, LineCandle, SettingsValues } from '@src/types';
+import { gains, gainsSma, rs, rsi, rsiMa } from './rsi';
+
export type ChartTypeToCandleData = {
['Bar']: Candle;
['Candlestick']: Candle;
@@ -52,6 +54,7 @@ export const indicatorsMap = (): Partial<Record<IndicatorsIds, IndicatorConfig>>
},
],
},
+
[IndicatorsIds.SMA]: {
series: [
{
@@ -78,6 +81,7 @@ export const indicatorsMap = (): Partial<Record<IndicatorsIds, IndicatorConfig>>
{ type: 'number', key: 'offset', label: t('Offset'), defaultValue: 0, min: -100, max: 100 },
],
},
+
[IndicatorsIds.EMA]: {
paletteStartIndex: INDICATOR_COLOR_PALETTE_MIDDLE_INDEX,
series: [
@@ -205,4 +209,120 @@ export const indicatorsMap = (): Partial<Record<IndicatorsIds, IndicatorConfig>>
},
],
},
+
+ [IndicatorsIds.RSI]: {
+ newPane: true,
+ series: [
+ {
+ name: 'Line', // todo: change with enum
+ actLikeMainSerie: true,
+ id: 'gains',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => gains(params as IndicatorDataFormatter<'Line'>, true),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: false,
+ },
+ },
+ {
+ name: 'Line', // todo: change with enum
+ actLikeMainSerie: true,
+ id: 'losses',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => gains(params as IndicatorDataFormatter<'Line'>, false),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: false,
+ },
+ },
+ {
+ name: 'Line', // todo: change with enum
+ actLikeMainSerie: true,
+ id: 'gains_sma',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => gainsSma(params as IndicatorDataFormatter<'Line'>, 'gains'),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: false,
+ },
+ },
+ {
+ name: 'Line', // todo: change with enum
+ actLikeMainSerie: true,
+ id: 'losses_sma',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => gainsSma(params as IndicatorDataFormatter<'Line'>, 'losses'),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: false,
+ },
+ },
+ {
+ name: 'Line', // todo: change with enum
+ actLikeMainSerie: true,
+ id: 'RS',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => rs(params as IndicatorDataFormatter<'Line'>),
+ seriesOptions: {
+ visible: false,
+ lastValueVisible: false,
+ },
+ },
+ {
+ name: 'Line', // todo: change with enum
+ actLikeMainSerie: true,
+ id: 'RSI',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => rsi(params as IndicatorDataFormatter<'Line'>),
+ seriesOptions: {
+ visible: true,
+ lastValueVisible: true,
+ },
+ },
+ {
+ name: 'Line', // todo: change with enum
+ actLikeMainSerie: true,
+ id: 'RSI_ma',
+ priceScaleOptions: {
+ mode: PriceScaleMode.Normal,
+ },
+ dataFormatter: (params) => rsiMa(params as IndicatorDataFormatter<'Line'>),
+ seriesOptions: {
+ visible: true,
+ lastValueVisible: true,
+ },
+ onSerieInit: (series) => {
+ if (series) {
+ series.createPriceLine({
+ price: 70,
+ color: '#787b86',
+ lineWidth: 1,
+ lineStyle: 2, // dashed line
+ axisLabelVisible: false,
+ });
+ series.createPriceLine({
+ price: 30,
+ color: '#787b86',
+ lineWidth: 1,
+ lineStyle: 2, // dashed line
+ axisLabelVisible: false,
+ });
+ }
+ },
+ },
+ ],
+ settings: [{ type: 'number', key: 'MA_Length', label: t('MA length'), defaultValue: 14, min: 5, max: 500 }],
+ },
});
diff --git a/src/core/Indicators/rma.ts b/src/core/Indicators/rma.ts
new file mode 100644
index 0000000000000000000000000000000000000000..955294ef93c92aa50fe458b8cbedcbc46f3f71cc
--- /dev/null
+++ b/src/core/Indicators/rma.ts
@@ -0,0 +1,69 @@
+import { SeriesDataItemTypeMap, Time } from 'lightweight-charts';
+
+import { LineCandle } from '@src/types';
+
+import { isLineData } from '@src/utils';
+
+import { SerieData } from '../Series/BaseSeries';
+
+import { ChartTypeToCandleData } from '.';
+
+export function calculatePreciseRMASeriesData(
+ selfData: SeriesDataItemTypeMap<Time>['Line'][],
+ candle: ChartTypeToCandleData['Line'],
+ rmaLength: number,
+): SeriesDataItemTypeMap<Time>['Line'] {
+ const alpha = 1 / rmaLength;
+ const prevLineData = selfData.at(-1);
+
+ if (!prevLineData || !isLineData(prevLineData)) {
+ return {
+ time: candle.time as Time,
+ };
+ }
+
+ return {
+ time: candle.time as Time,
+ value: alpha * candle.value + (1 - alpha) * prevLineData.value,
+ };
+}
+
+export function calculateRMASeriesData(
+ candleData: LineCandle[],
+ rmaLength: number,
+): SeriesDataItemTypeMap<Time>['Line'][] {
+ const rma: SeriesDataItemTypeMap<Time>['Line'][] = candleData.map((lineCandle) => ({
+ time: lineCandle.time as Time,
+ }));
+ const alpha = 1 / rmaLength;
+ if (candleData.length < rmaLength) {
+ return rma;
+ }
+
+ let sum = 0;
+
+ for (let i = 0; i < rmaLength; i++) {
+ sum += candleData[i].value;
+ }
+
+ rma[rmaLength - 1] = {
+ ...rma[rmaLength - 1],
+ value: sum / rmaLength,
+ };
+
+ for (let i = rmaLength; i < candleData.length; i++) {
+ const candle = candleData[i];
+ const prevRMA = rma[i - 1];
+
+ if (!isLineData(prevRMA)) {
+ continue;
+ }
+
+ rma[i] = {
+ ...rma[i],
+ value: alpha * candle.value + (1 - alpha) * prevRMA.value,
+ };
+ }
+
+ return rma;
+}
diff --git a/src/core/Indicators/rsi.ts b/src/core/Indicators/rsi.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bcac80088f43ea321925e456ce447447bab407d6
--- /dev/null
+++ b/src/core/Indicators/rsi.ts
@@ -0,0 +1,235 @@
+import { SeriesDataItemTypeMap, Time, WhitespaceData } from 'lightweight-charts';
+
+import { LineCandle } from '@src/types';
+import { isLineData } from '@src/utils';
+
+import { calculatePreciseRMASeriesData, calculateRMASeriesData } from './rma';
+import { calculateMASeriesData, calculatePreciseMASeriesData } from './sma';
+
+import { ChartTypeToCandleData, IndicatorDataFormatter } from '.';
+
+export function gains(
+ { candle, mainSeriesData, selfData }: IndicatorDataFormatter<'Line'>,
+ isGain: boolean,
+): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!mainSeriesData) return [];
+
+ if (!candle) {
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = mainSeriesData.map((lineCandle) => ({ time: lineCandle.time }));
+
+ mainSeriesData.forEach((bar, index, self) => {
+ const change: number = bar.customValues.close - (self[index - 1]?.customValues.close ?? 0);
+
+ if ((isGain && change >= 0) || (!isGain && change < 0)) {
+ res[index] = {
+ time: bar.time,
+ value: change,
+ };
+ } else {
+ res[index] = {
+ time: bar.time,
+ value: 0,
+ };
+ }
+ });
+ return res;
+ }
+
+ const prevCandleClose =
+ mainSeriesData.at(-1)?.time === candle.time
+ ? mainSeriesData.at(-2)?.customValues.close
+ : mainSeriesData.at(-1)?.customValues.close;
+
+ if (!prevCandleClose) {
+ return [
+ {
+ time: candle.time,
+ },
+ ];
+ }
+
+ const change: number = candle.customValues.close - prevCandleClose;
+
+ if ((isGain && change >= 0) || (!isGain && change < 0)) {
+ return [
+ {
+ time: candle.time as Time,
+ value: change,
+ },
+ ];
+ }
+ return [
+ {
+ time: candle.time,
+ value: 0,
+ },
+ ];
+}
+
+export function gainsSma(
+ { candle, indicatorReference, settings, selfData }: IndicatorDataFormatter<'Line'>,
+ basedOn: string,
+): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!indicatorReference) return [];
+ const signalLength = typeof settings?.MA_Length === 'number' ? settings.MA_Length : 14;
+
+ const gainsSerie = (indicatorReference.getSeriesMap().get(basedOn)?.data() ??
+ []) as unknown as ChartTypeToCandleData['Line'][];
+
+ if (!candle) {
+ return calculateRMASeriesData(gainsSerie, signalLength);
+ }
+ const lastGains = gainsSerie.at(-1)?.time === candle.time ? gainsSerie.at(-1) : undefined;
+ if (!lastGains) {
+ return [
+ {
+ time: candle.time,
+ },
+ ];
+ }
+
+ return [calculatePreciseRMASeriesData(selfData as SeriesDataItemTypeMap<Time>['Line'][], lastGains, signalLength)];
+}
+
+export function rs(params: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
+ const { candle, indicatorReference, settings, mainSeriesData } = params;
+ const signalLength = typeof settings?.MA_Length === 'number' ? settings.MA_Length : 14;
+
+ if (!indicatorReference) return [];
+ if (!mainSeriesData) return [];
+ const avgGains = (indicatorReference.getSeriesMap().get('gains_sma')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+ const avgLosses = (indicatorReference.getSeriesMap().get('losses_sma')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+
+ const rsSeries: SeriesDataItemTypeMap<Time>['Line'][] = mainSeriesData.map((lineCandle) => ({
+ time: lineCandle.time,
+ }));
+
+ if (mainSeriesData.length <= signalLength) return rsSeries;
+
+ const infinity = Number.MIN_SAFE_INTEGER;
+
+ if (!candle) {
+ for (let i = signalLength; i < mainSeriesData.length; i++) {
+ const avgGain = avgGains[i - signalLength];
+ const avgLoss = avgLosses[i - signalLength];
+
+ if (!isLineData(avgGain) || !isLineData(avgLoss)) {
+ continue;
+ }
+
+ if (avgLoss.value === 0) {
+ rsSeries[i] = {
+ time: avgGain.time as Time,
+ value: avgGain.value === 0 ? 1 : infinity,
+ };
+ } else {
+ rsSeries[i] = {
+ time: avgGain.time as Time,
+ value: avgGain.value / Math.abs(avgLoss.value),
+ };
+ }
+ }
+
+ return rsSeries.map((bar) => {
+ if (isLineData(bar)) {
+ return {
+ ...bar,
+ value: +bar.value.toFixed(2),
+ };
+ }
+ return bar;
+ });
+ }
+
+ const avgGain = avgGains.at(-1)?.time === candle.time ? avgGains.at(-1) : undefined;
+ const avgLoss = avgLosses.at(-1)?.time === candle.time ? avgLosses.at(-1) : undefined;
+
+ if (
+ !avgGain ||
+ !isLineData(avgGain) ||
+ !avgLoss ||
+ !isLineData(avgLoss) ||
+ avgGain.time !== candle.time ||
+ avgLoss.time !== candle.time
+ ) {
+ return [{ time: candle.time }];
+ }
+
+ if (avgLoss.value === 0) {
+ return [
+ {
+ time: avgGain.time as Time,
+ value: avgGain.value === 0 ? 1 : infinity,
+ },
+ ];
+ }
+ return [
+ {
+ time: avgGain.time as Time,
+ value: avgGain.value / Math.abs(avgLoss.value),
+ },
+ ];
+}
+
+export function rsi({
+ candle,
+ indicatorReference,
+}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!indicatorReference) return [];
+
+ const rsSerie = (indicatorReference.getSeriesMap().get('RS')?.data() ??
+ []) as unknown as SeriesDataItemTypeMap['Line'][];
+
+ if (!candle) {
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = [];
+ rsSerie.forEach((lineCandle) => {
+ if (!isLineData(lineCandle)) {
+ res.push({
+ time: lineCandle.time,
+ });
+ } else {
+ res.push({
+ ...lineCandle,
+ value: +(100 - 100 / (1 + lineCandle.value)).toFixed(2),
+ });
+ }
+ });
+
+ return res;
+ }
+
+ const lineCandle = rsSerie.at(-1)?.time === candle.time ? rsSerie.at(-1) : undefined;
+
+ if (!lineCandle || !isLineData(lineCandle) || lineCandle.time !== candle.time) {
+ return [{ time: candle.time }];
+ }
+
+ return [
+ {
+ ...lineCandle,
+ value: +(100 - 100 / (1 + lineCandle.value)).toFixed(2),
+ },
+ ];
+}
+
+export function rsiMa({
+ candle,
+ indicatorReference,
+ settings,
+}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
+ const signalLength = typeof settings?.MA_Length === 'number' ? settings.MA_Length : 14;
+
+ if (!indicatorReference) {
+ return [];
+ }
+
+ const rsiSerie = (indicatorReference.getSeriesMap().get('RSI')?.data() ?? []) as unknown as LineCandle[];
+
+ if (!candle) {
+ return calculateMASeriesData(rsiSerie, signalLength);
+ }
+
+ return [calculatePreciseMASeriesData(rsiSerie, candle, signalLength)];
+}
diff --git a/src/translations/russianDict.ts b/src/translations/russianDict.ts
index c9e375c6967ad81b105f902fa38aeb276e77f780..2259d0358d59ffab1db7d562b77257ecfdf73d57 100644
--- a/src/translations/russianDict.ts
+++ b/src/translations/russianDict.ts
@@ -27,7 +27,7 @@ export const russian = {
week_few: 'недели',
week_many: 'недель',
month: 'месяц',
- months: 'месяца',
+ months: 'месяцы',
month_few: 'месяца',
month_many: 'месяцев',
Bars: 'Бары',
@@ -129,6 +129,7 @@ export const russian = {
'Signal length': 'Длина сигнала',
'Oscillator MA type': 'Тип осцилятора MA',
'Signal MA type': 'Тип сигнала MA',
+ 'MA length': 'Длина MA',
Save: 'Сохранить',
Reject: 'Отменить',
Volume: 'Объём',
diff --git a/src/types/indicator.ts b/src/types/indicator.ts
index 036fd00f8357362b3f809ab866e3909e0dd154d8..26b504ebc8ce209d141ec8c6c4760f3380d034f8 100644
--- a/src/types/indicator.ts
+++ b/src/types/indicator.ts
@@ -13,6 +13,7 @@ import {
import { indicatorLabelById, IndicatorsIds } from '@src/constants';
import { IndicatorDataFormatter } from '@src/core/Indicators';
+import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ChartSeriesType } from '@src/types/chart';
import { SettingField } from '@src/types/settings';
import { SymbolInfoInput } from '@src/types/symbol';
@@ -38,6 +39,7 @@ export interface IndicatorSerie {
name: ChartSeriesType;
actLikeMainSerie?: boolean;
seriesOptions?: SeriesPartialOptionsMap[ChartSeriesType];
+ onSerieInit?: (series: SeriesStrategies) => void;
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
priceScaleId?: string;
dataFormatter?<T extends SeriesType>(params: IndicatorDataFormatter<T>): SeriesDataItemTypeMap<Time>[T][];
diff --git a/stories/TradeRadar/TradeRadar.stories.tsx b/stories/TradeRadar/TradeRadar.stories.tsx
index 854c8b2a4dc0a18a036fb16fb2090e03e4b562e2..ce6cfa363e4f2cedb021ec2ddf320d7581d6b295 100644
--- a/stories/TradeRadar/TradeRadar.stories.tsx
+++ b/stories/TradeRadar/TradeRadar.stories.tsx
@@ -146,11 +146,6 @@ const args: TRProps = {
{
indicatorType: IndicatorsIds.EMA,
},
- {
- symbolInfo: { symbolId: 'TQBR:SBER' },
- scale: Direction.Right,
- seriesName: 'Line',
- },
],
drawings: [],
},
@@ -159,7 +154,7 @@ const args: TRProps = {
id: 1,
indicators: [
{
- indicatorType: IndicatorsIds.MACD,
+ indicatorType: IndicatorsIds.RSI,
},
],
drawings: [],