You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ghostfolio/libs/common/src/lib/helper.ts

400 lines
9.2 KiB

import * as currencies from '@dinero.js/currencies';
import { DataSource, MarketData } from '@prisma/client';
import Big from 'big.js';
import {
getDate,
getMonth,
getYear,
isMatch,
parse,
parseISO,
subDays
} from 'date-fns';
import { de, es, fr, it, nl, pl, pt, tr } from 'date-fns/locale';
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
import { Benchmark, UniqueAsset } from './interfaces';
import { BenchmarkTrend, ColorScheme } from './types';
export const DATE_FORMAT = 'yyyy-MM-dd';
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
export const DATE_FORMAT_YEARLY = 'yyyy';
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
export function calculateBenchmarkTrend({
days,
historicalData
}: {
days: number;
historicalData: MarketData[];
}): BenchmarkTrend {
const hasEnoughData = historicalData.length >= 2 * days;
if (!hasEnoughData) {
return 'UNKNOWN';
}
const recentPeriodAverage = calculateMovingAverage({
days,
prices: historicalData.slice(0, days).map(({ marketPrice }) => {
return new Big(marketPrice);
})
});
const pastPeriodAverage = calculateMovingAverage({
days,
prices: historicalData.slice(days, 2 * days).map(({ marketPrice }) => {
return new Big(marketPrice);
})
});
if (recentPeriodAverage > pastPeriodAverage) {
return 'UP';
}
if (recentPeriodAverage < pastPeriodAverage) {
return 'DOWN';
}
return 'NEUTRAL';
}
export function calculateMovingAverage({
days,
prices
}: {
days: number;
prices: Big[];
}) {
return prices
.reduce((previous, current) => {
return previous.add(current);
}, new Big(0))
.div(days)
.toNumber();
}
export function capitalize(aString: string) {
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
}
export function decodeDataSource(encodedDataSource: string) {
if (encodedDataSource) {
return Buffer.from(encodedDataSource, 'hex').toString();
}
return undefined;
}
export function downloadAsFile({
content,
contentType = 'text/plain',
fileName,
format
}: {
content: unknown;
contentType?: string;
fileName: string;
format: 'json' | 'string';
}) {
const a = document.createElement('a');
if (format === 'json') {
content = JSON.stringify(content, undefined, ' ');
}
const file = new Blob([<string>content], {
type: contentType
});
a.href = URL.createObjectURL(file);
a.download = fileName;
a.click();
}
export function encodeDataSource(aDataSource: DataSource) {
if (aDataSource) {
return Buffer.from(aDataSource, 'utf-8').toString('hex');
}
return undefined;
}
export function extractNumberFromString(aString: string): number {
try {
const [numberString] = aString.match(NUMERIC_REGEXP);
return parseFloat(numberString.trim());
} catch {
return undefined;
}
}
export function getAssetProfileIdentifier({ dataSource, symbol }: UniqueAsset) {
return `${dataSource}-${symbol}`;
}
export function getAverage(aArray: Big[]) {
if (aArray?.length > 0) {
return aArray.reduce((a, b) => a.plus(b), new Big(0)).div(aArray.length);
}
return new Big(0);
}
export function getBackgroundColor(aColorScheme: ColorScheme) {
return getCssVariable(
aColorScheme === 'DARK' ||
window.matchMedia('(prefers-color-scheme: dark)').matches
? '--dark-background'
: '--light-background'
);
}
export function getCssVariable(aCssVariable: string) {
return getComputedStyle(document.documentElement).getPropertyValue(
aCssVariable
);
}
export function getDateFnsLocale(aLanguageCode: string) {
if (aLanguageCode === 'de') {
return de;
} else if (aLanguageCode === 'es') {
return es;
} else if (aLanguageCode === 'fr') {
return fr;
} else if (aLanguageCode === 'it') {
return it;
} else if (aLanguageCode === 'nl') {
return nl;
} else if (aLanguageCode === 'pl') {
return pl;
} else if (aLanguageCode === 'pt') {
return pt;
} else if (aLanguageCode === 'tr') {
return tr;
}
return undefined;
}
export function getDateFormatString(aLocale?: string) {
const formatObject = new Intl.DateTimeFormat(aLocale).formatToParts(
new Date()
);
return formatObject
.map((object) => {
switch (object.type) {
case 'day':
return 'dd';
case 'month':
return 'MM';
case 'year':
return 'yyyy';
default:
return object.value;
}
})
.join('');
}
export function getDateWithTimeFormatString(aLocale?: string) {
return `${getDateFormatString(aLocale)}, HH:mm:ss`;
}
export function getEmojiFlag(aCountryCode: string) {
if (!aCountryCode) {
return aCountryCode;
}
return aCountryCode
.toUpperCase()
.replace(/./g, (character) =>
String.fromCodePoint(127397 + character.charCodeAt(0))
);
}
export function getLocale() {
return navigator.languages?.length
? navigator.languages[0]
: navigator.language ?? locale;
}
export function getNumberFormatDecimal(aLocale?: string) {
const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
return formatObject.find((object) => {
return object.type === 'decimal';
}).value;
}
export function getNumberFormatGroup(aLocale?: string) {
const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
return formatObject.find((object) => {
return object.type === 'group';
}).value;
}
export function getStartOfUtcDate(aDate: Date) {
const date = new Date(aDate);
date.setUTCHours(0, 0, 0, 0);
return date;
}
export function getSum(aArray: Big[]) {
if (aArray?.length > 0) {
return aArray.reduce((a, b) => a.plus(b), new Big(0));
}
return new Big(0);
}
export function getTextColor(aColorScheme: ColorScheme) {
const cssVariable = getCssVariable(
aColorScheme === 'DARK' ||
window.matchMedia('(prefers-color-scheme: dark)').matches
? '--light-primary-text'
: '--dark-primary-text'
);
const [r, g, b] = cssVariable.split(',');
return `${r}, ${g}, ${b}`;
}
export function getToday() {
const year = getYear(new Date());
const month = getMonth(new Date());
const day = getDate(new Date());
return new Date(Date.UTC(year, month, day));
}
export function getUtc(aDateString: string) {
const [yearString, monthString, dayString] = aDateString.split('-');
return new Date(
Date.UTC(
parseInt(yearString, 10),
parseInt(monthString, 10) - 1,
parseInt(dayString, 10)
)
);
}
export function getYesterday() {
const year = getYear(new Date());
const month = getMonth(new Date());
const day = getDate(new Date());
return subDays(new Date(Date.UTC(year, month, day)), 1);
}
export function groupBy<T, K extends keyof T>(
key: K,
arr: T[]
): Map<T[K], T[]> {
const map = new Map<T[K], T[]>();
arr.forEach((t) => {
if (!map.has(t[key])) {
map.set(t[key], []);
}
map.get(t[key])!.push(t);
});
return map;
}
export function interpolate(template: string, context: any) {
return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
const properties = objectPath.split('.');
return properties.reduce(
(previous, current) => previous?.[current],
context
);
});
}
export function isCurrency(aSymbol = '') {
return currencies[aSymbol];
}
export function parseDate(date: string): Date | null {
// Transform 'yyyyMMdd' format to supported format by parse function
if (date?.length === 8) {
const match = date.match(/^(\d{4})(\d{2})(\d{2})$/);
if (match) {
const [, year, month, day] = match;
date = `${year}-${month}-${day}`;
}
}
const dateFormat = [
'dd-MM-yyyy',
'dd/MM/yyyy',
'dd.MM.yyyy',
'yyyy-MM-dd',
'yyyy/MM/dd',
'yyyy.MM.dd',
'yyyyMMdd'
].find((format) => {
return isMatch(date, format) && format.length === date.length;
});
if (dateFormat) {
return parse(date, dateFormat, new Date());
}
return parseISO(date);
}
export function parseSymbol({ dataSource, symbol }: UniqueAsset) {
const [ticker, exchange] = symbol.split('.');
return {
ticker,
exchange: exchange ?? (dataSource === 'YAHOO' ? 'US' : undefined)
};
}
export function prettifySymbol(aSymbol: string): string {
return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, '');
}
export function resetHours(aDate: Date) {
const year = getYear(aDate);
const month = getMonth(aDate);
const day = getDate(aDate);
return new Date(Date.UTC(year, month, day));
}
export function resolveFearAndGreedIndex(aValue: number) {
if (aValue <= 25) {
return { emoji: '🥵', key: 'EXTREME_FEAR', text: 'Extreme Fear' };
} else if (aValue <= 45) {
return { emoji: '😨', key: 'FEAR', text: 'Fear' };
} else if (aValue <= 55) {
return { emoji: '😐', key: 'NEUTRAL', text: 'Neutral' };
} else if (aValue < 75) {
return { emoji: '😜', key: 'GREED', text: 'Greed' };
} else {
return { emoji: '🤪', key: 'EXTREME_GREED', text: 'Extreme Greed' };
}
}
export function resolveMarketCondition(
aMarketCondition: Benchmark['marketCondition']
) {
if (aMarketCondition === 'ALL_TIME_HIGH') {
return { emoji: '🎉' };
} else if (aMarketCondition === 'BEAR_MARKET') {
return { emoji: '🐻' };
} else {
return { emoji: '⚪' };
}
}