Загрузка данных
import { LineData, SeriesDataItemTypeMap, Time } from 'lightweight-charts';
import { SerieData } from '@core/Series/BaseSeries';
import { LineCandle } from '../../types';
import { IndicatorDataFormatter } from './index';
type MASource = 'open' | 'high' | 'low' | 'close';
const MA_SOURCES: MASource[] = ['open', 'high', 'low', 'close'];
export function emaIndicator(
{ mainSeriesData, selfData, candle, settings }: IndicatorDataFormatter<'Line'>,
defaultLength = 25,
): SeriesDataItemTypeMap<Time>['Line'][] {
const length = getNumberSetting(settings, 'length', defaultLength);
const offset = getNumberSetting(settings, 'offset', 0);
const source = getSourceSetting(settings);
const sourceData = mainSeriesData.map((point) => {
const values = point.customValues as Record<string, unknown>;
const sourceValue = values[source];
const fallbackValue = values.close ?? values.value;
return {
time: values.time as Time,
value:
typeof sourceValue === 'number'
? sourceValue
: typeof fallbackValue === 'number'
? fallbackValue
: 0,
};
});
if (!candle || offset !== 0) {
const calculatedSeries = calculateEMASeriesData(sourceData, length);
if (offset === 0) {
return calculatedSeries;
}
const shiftedSeries: SeriesDataItemTypeMap<Time>['Line'][] = [];
for (let index = 0; index < calculatedSeries.length; index += 1) {
const targetIndex = index + offset;
if (targetIndex < 0 || targetIndex >= sourceData.length) {
continue;
}
const point = calculatedSeries[index];
const targetTime = sourceData[targetIndex].time as Time;
if ('value' in point && typeof point.value === 'number') {
shiftedSeries.push({
time: targetTime,
value: point.value,
});
} else {
shiftedSeries.push({
time: targetTime,
});
}
}
return shiftedSeries;
}
if (!selfData) {
return [{ time: candle.time as Time, value: 0 }];
}
return [calculatePreciseEMASeriesData(sourceData, selfData, candle, length)];
}
export function calculatePreciseEMASeriesData(
candleData: LineCandle[],
currentIndicatorData: LineCandle[],
candle: SerieData,
maLength: number,
): SeriesDataItemTypeMap<Time>['Line'] {
const candleIndexToCalculate = candleData.findIndex((c) => c.time === candle.time);
if (candleIndexToCalculate === -1) {
console.error('[Indicators]: нет подходящей свечи в массиве');
return {
time: candle.time as Time,
value: 0,
};
}
const prevCandleIndex = currentIndicatorData.length - 2;
const prevCandle = currentIndicatorData[prevCandleIndex];
const smoothing = 2;
const k = smoothing / (maLength + 1);
const prevEma = prevCandle?.value ?? 0;
const ema = k * candleData[candleIndexToCalculate].value + prevEma * (1 - k);
return {
time: candle.time as Time,
value: ema,
};
}
export function calculateEMASeriesData(
candleData: LineCandle[],
maLength: number,
): SeriesDataItemTypeMap<Time>['Line'][] {
// todo: change signature to {time & value}
const maData: LineData<Time>[] = [];
const smoothing = 2;
const k = smoothing / (maLength + 1);
for (let i = 0; i < candleData.length; i++) {
if (i < maLength - 1) {
// Provide whitespace data points until the MA can be calculated
maData.push({ time: candleData[i].time as Time, value: 0 });
} else if (i === maLength - 1) {
let sum = 0;
for (let j = 0; j < maLength; j++) {
sum += candleData[i - j].value;
}
const maValue = sum / maLength;
maData.push({
time: candleData[i].time as Time,
value: maValue,
});
} else {
const prevEma = maData[maData.length - 1]?.value ?? 0;
const ema = k * candleData[i].value + prevEma * (1 - k);
maData.push({
time: candleData[i].time as Time,
value: ema,
});
}
}
return maData;
}
function getNumberSetting(
settings: IndicatorDataFormatter<'Line'>['settings'],
key: string,
defaultValue: number,
): number {
const value = settings?.[key];
return typeof value === 'number' ? value : defaultValue;
}
function getSourceSetting(settings: IndicatorDataFormatter<'Line'>['settings']): MASource {
const value = settings?.source;
return typeof value === 'string' && isMASource(value) ? value : 'close';
}
function isMASource(value: string): value is MASource {
return MA_SOURCES.includes(value as MASource);
}
import { SeriesDataItemTypeMap, Time } from 'lightweight-charts';
import { IndicatorDataFormatter } from '@core/Indicators/index';
import { SerieData } from '@core/Series/BaseSeries';
import { LineCandle } from '@src/types';
type MASource = 'open' | 'high' | 'low' | 'close';
const MA_SOURCES: MASource[] = ['open', 'high', 'low', 'close'];
export function smaIndicator(
{ mainSeriesData, candle, settings }: IndicatorDataFormatter<'Line'>,
defaultLength = 10,
): SeriesDataItemTypeMap<Time>['Line'][] {
const length = getNumberSetting(settings, 'length', defaultLength);
const offset = getNumberSetting(settings, 'offset', 0);
const source = getSourceSetting(settings);
const sourceData = mainSeriesData.map((point) => {
const values = point.customValues as Record<string, unknown>;
const sourceValue = values[source];
const fallbackValue = values.close ?? values.value;
return {
time: values.time as Time,
value:
typeof sourceValue === 'number'
? sourceValue
: typeof fallbackValue === 'number'
? fallbackValue
: 0,
};
});
if (!candle || offset !== 0) {
const calculatedSeries = calculateMASeriesData(sourceData, length);
if (offset === 0) {
return calculatedSeries;
}
const shiftedSeries: SeriesDataItemTypeMap<Time>['Line'][] = [];
for (let index = 0; index < calculatedSeries.length; index += 1) {
const targetIndex = index + offset;
if (targetIndex < 0 || targetIndex >= sourceData.length) {
continue;
}
const point = calculatedSeries[index];
const targetTime = sourceData[targetIndex].time as Time;
if ('value' in point && typeof point.value === 'number') {
shiftedSeries.push({
time: targetTime,
value: point.value,
});
} else {
shiftedSeries.push({
time: targetTime,
});
}
}
return shiftedSeries;
}
return [calculatePreciseMASeriesData(sourceData, candle, length)];
}
export function calculatePreciseMASeriesData(
candleData: LineCandle[],
candle: SerieData,
maLength: number,
): SeriesDataItemTypeMap<Time>['Line'] {
const candleIndexToCalculate = candleData.findIndex((c) => c.time === candle.time);
if (candleIndexToCalculate === -1) {
console.error('[Indicators]: нет подходящей свечи в массиве');
return {
time: candle.time as Time,
value: 0,
};
}
let sum = 0;
for (let i = candleIndexToCalculate; i > candleIndexToCalculate - maLength; i--) {
if (i < 0) break;
sum += candleData[i].value;
}
return {
time: candle.time as Time,
value: sum / maLength,
};
}
export function calculateMASeriesData(
candleData: LineCandle[],
maLength: number,
): SeriesDataItemTypeMap<Time>['Line'][] {
const maData: SeriesDataItemTypeMap<Time>['Line'][] = [];
for (let i = 0; i < candleData.length; i++) {
if (i < maLength) {
// Provide whitespace data points until the MA can be calculated
maData.push({ time: candleData[i].time as Time });
} else {
// Calculate the moving average, slow but simple way
let sum = 0;
for (let j = 0; j < maLength; j++) {
sum += candleData[i - j].value;
}
const maValue = sum / maLength;
maData.push({
time: candleData[i].time as Time,
value: maValue,
});
}
}
return maData;
}
function getNumberSetting(
settings: IndicatorDataFormatter<'Line'>['settings'],
key: string,
defaultValue: number,
): number {
const value = settings?.[key];
return typeof value === 'number' ? value : defaultValue;
}
function getSourceSetting(settings: IndicatorDataFormatter<'Line'>['settings']): MASource {
const value = settings?.source;
return typeof value === 'string' && isMASource(value) ? value : 'close';
}
function isMASource(value: string): value is MASource {
return MA_SOURCES.includes(value as MASource);
}