Загрузка данных
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0db5328422a945504ca18a7a3f86aa97b0fc04a..f1124ea6bde8f61edf30fb2dcefa7cb9381e30ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
- Добавлена возможность работы с дровингами на второстепенных пейнах
- Исправлено взаимодействие с пересекающимися дровингами
+- Добавлен индикатор RSI
+- Исправлен баг, когда линия EMA уходила в будущее
# 0.1.19
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 c232d4eb4fd9dbb102c3cde42070996fa73f0cb5..e51b250d9a2571bb3a4aa06b06f41fe8ceed655d 100644
--- a/src/core/Indicator.ts
+++ b/src/core/Indicator.ts
@@ -211,7 +211,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,
@@ -234,6 +234,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 1a61e79c863352811f8f3c8c756987f04555a4d0..befa104cc79326ab866ed79f3140441bf36a749c 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 { Direction, 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: [
@@ -209,4 +213,127 @@ 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: {
+ priceScaleId: Direction.Right,
+ 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: {
+ priceScaleId: Direction.Right,
+ 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: {
+ priceScaleId: Direction.Right,
+ 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: {
+ priceScaleId: Direction.Right,
+ 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: {
+ priceScaleId: Direction.Right,
+ 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: {
+ priceScaleId: Direction.Right,
+ 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: {
+ priceScaleId: Direction.Right,
+ 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: 12, min: 1, max: 500 }],
+ },
});
diff --git a/src/core/Indicators/rsi.ts b/src/core/Indicators/rsi.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c9460528ad7d48b22a83593143789fa9bb2453a4
--- /dev/null
+++ b/src/core/Indicators/rsi.ts
@@ -0,0 +1,222 @@
+import { SeriesDataItemTypeMap, Time, WhitespaceData } from 'lightweight-charts';
+
+import { LineCandle } from '@src/types';
+import { isLineData } from '@src/utils';
+
+import { calculateMASeriesData, calculatePreciseMASeriesData } from './sma';
+
+import { ChartTypeToCandleData, IndicatorDataFormatter } from '.';
+
+export function gains(
+ { candle, mainSeriesData }: IndicatorDataFormatter<'Line'>,
+ isGain: boolean,
+): SeriesDataItemTypeMap<Time>['Line'][] {
+ if (!mainSeriesData) return [];
+
+ if (!candle) {
+ const res: SeriesDataItemTypeMap<Time>['Line'][] = new Array(mainSeriesData.length).fill(null);
+
+ mainSeriesData.forEach((bar, index) => {
+ const change: number = bar.customValues.close - bar.customValues.open;
+
+ 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 change: number = candle.customValues.close - candle.customValues.open;
+
+ 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 }: 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 calculateMASeriesData(gainsSerie, signalLength);
+ }
+
+ return [calculatePreciseMASeriesData(gainsSerie, candle, 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'][] = new Array(mainSeriesData.length).fill(null);
+
+ if (mainSeriesData.length <= signalLength) return rsSeries;
+
+ const infinity = 999999;
+
+ if (!candle) {
+ const appendix = mainSeriesData
+ .slice(0, signalLength)
+ .map((d) => ({ time: d.time })) as unknown as WhitespaceData<Time>[];
+
+ avgGains.unshift(...appendix);
+ avgLosses.unshift(...appendix);
+
+ for (let i = 0; i < mainSeriesData.length; i++) {
+ const avgGain = avgGains[i];
+ const avgLoss = avgLosses[i];
+
+ if (!isLineData(avgGain) || !isLineData(avgLoss)) {
+ rsSeries[i] = {
+ time: mainSeriesData[i].time,
+ };
+ 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);
+ const avgLoss = avgLosses.at(-1);
+
+ 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: Math.floor(100 - 100 / (1 + lineCandle.value)),
+ });
+ }
+ });
+
+ return res;
+ }
+
+ const lineCandle = rsSerie.at(-1);
+
+ if (!lineCandle || !isLineData(lineCandle) || lineCandle.time !== candle.time) {
+ return [{ time: candle.time }];
+ }
+
+ return [
+ {
+ ...lineCandle,
+ value: Math.floor(100 - 100 / (1 + lineCandle.value)),
+ },
+ ];
+}
+
+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 8ca595b3a581b1901e7d61eb2741daa215e3667f..2e737f6c6aea42ac01f01201c090b39ba25a9e13 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: 'Бары',
@@ -128,6 +128,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][];