mirror of https://github.com/ghostfolio/ghostfolio
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.
214 lines
6.0 KiB
214 lines
6.0 KiB
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
|
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
|
|
import { DateRange } from '@ghostfolio/api/app/portfolio/interfaces/date-range.interface';
|
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
|
import { resetHours } from '@ghostfolio/common/helper';
|
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
|
|
|
import { Injectable } from '@nestjs/common';
|
|
import {
|
|
DataSource,
|
|
MarketData,
|
|
MarketDataState,
|
|
Prisma
|
|
} from '@prisma/client';
|
|
|
|
@Injectable()
|
|
export class MarketDataService {
|
|
public constructor(private readonly prismaService: PrismaService) {}
|
|
|
|
public async deleteMany({ dataSource, symbol }: UniqueAsset) {
|
|
return this.prismaService.marketData.deleteMany({
|
|
where: {
|
|
dataSource,
|
|
symbol
|
|
}
|
|
});
|
|
}
|
|
|
|
public async get({
|
|
dataSource,
|
|
date = new Date(),
|
|
symbol
|
|
}: IDataGatheringItem): Promise<MarketData> {
|
|
return await this.prismaService.marketData.findFirst({
|
|
where: {
|
|
dataSource,
|
|
symbol,
|
|
date: resetHours(date)
|
|
}
|
|
});
|
|
}
|
|
|
|
public async getMax({ dataSource, symbol }: UniqueAsset) {
|
|
return this.prismaService.marketData.findFirst({
|
|
select: {
|
|
date: true,
|
|
marketPrice: true
|
|
},
|
|
orderBy: [
|
|
{
|
|
marketPrice: 'desc'
|
|
}
|
|
],
|
|
where: {
|
|
dataSource,
|
|
symbol
|
|
}
|
|
});
|
|
}
|
|
|
|
public async getRange({
|
|
dateQuery,
|
|
uniqueAssets
|
|
}: {
|
|
dateQuery: DateQuery;
|
|
uniqueAssets: UniqueAsset[];
|
|
}): Promise<MarketData[]> {
|
|
return this.prismaService.marketData.findMany({
|
|
orderBy: [
|
|
{
|
|
date: 'asc'
|
|
},
|
|
{
|
|
symbol: 'asc'
|
|
}
|
|
],
|
|
where: {
|
|
dataSource: {
|
|
in: uniqueAssets.map(({ dataSource }) => {
|
|
return dataSource;
|
|
})
|
|
},
|
|
date: dateQuery,
|
|
symbol: {
|
|
in: uniqueAssets.map(({ symbol }) => {
|
|
return symbol;
|
|
})
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
public async getDecimatedRange({
|
|
dateRange: { start, end, step },
|
|
symbols,
|
|
dataSources
|
|
}: {
|
|
dateRange: DateRange;
|
|
symbols: string[];
|
|
dataSources: DataSource[];
|
|
}): Promise<MarketData[]> {
|
|
return this.prismaService.$queryRaw`
|
|
WITH "lastDate" AS (
|
|
SELECT "date" FROM "MarketData"
|
|
WHERE "date" BETWEEN ${start} AND ${end}
|
|
ORDER BY "date" DESC LIMIT 1
|
|
), "lastRows" AS (
|
|
SELECT * FROM "MarketData"
|
|
WHERE "date" = (SELECT "date" FROM "lastDate")
|
|
AND "symbol" IN (${Prisma.join(symbols)})
|
|
AND "dataSource"::text IN (${Prisma.join(dataSources)})
|
|
)
|
|
SELECT DISTINCT
|
|
FIRST_VALUE("createdAt") OVER w AS "createdAt",
|
|
FIRST_VALUE("dataSource") OVER w AS "dataSource",
|
|
FIRST_VALUE("date") OVER w AS "date",
|
|
FIRST_VALUE("id") OVER w AS "id",
|
|
FIRST_VALUE("marketPrice") OVER w AS "marketPrice",
|
|
FIRST_VALUE("state") OVER w AS "state",
|
|
FIRST_VALUE("symbol") OVER w AS "symbol"
|
|
FROM "MarketData"
|
|
WHERE "date" BETWEEN ${start} AND ${end}
|
|
AND "symbol" IN (${Prisma.join(symbols)})
|
|
AND "dataSource"::text IN (${Prisma.join(dataSources)})
|
|
WINDOW w AS (
|
|
PARTITION BY "symbol", FLOOR(
|
|
(EXTRACT(EPOCH FROM "date") - EXTRACT(EPOCH FROM ${start})) -- Subtract {start} to make it first value
|
|
/ (60 * 60 * 24 * ${step}) -- Divide by {step} number of days
|
|
) ORDER BY "date" -- Round down to make every {step} values equal
|
|
)
|
|
UNION SELECT * FROM "lastRows" -- Add rows with the end date
|
|
ORDER BY "date"
|
|
`;
|
|
}
|
|
|
|
public async marketDataItems(params: {
|
|
select?: Prisma.MarketDataSelectScalar;
|
|
skip?: number;
|
|
take?: number;
|
|
cursor?: Prisma.MarketDataWhereUniqueInput;
|
|
where?: Prisma.MarketDataWhereInput;
|
|
orderBy?: Prisma.MarketDataOrderByWithRelationInput;
|
|
}): Promise<MarketData[]> {
|
|
const { select, skip, take, cursor, where, orderBy } = params;
|
|
|
|
return this.prismaService.marketData.findMany({
|
|
select,
|
|
cursor,
|
|
orderBy,
|
|
skip,
|
|
take,
|
|
where
|
|
});
|
|
}
|
|
|
|
public async updateMarketData(params: {
|
|
data: {
|
|
state: MarketDataState;
|
|
} & UpdateMarketDataDto;
|
|
where: Prisma.MarketDataWhereUniqueInput;
|
|
}): Promise<MarketData> {
|
|
const { data, where } = params;
|
|
|
|
return this.prismaService.marketData.upsert({
|
|
where,
|
|
create: {
|
|
dataSource: where.dataSource_date_symbol.dataSource,
|
|
date: where.dataSource_date_symbol.date,
|
|
marketPrice: data.marketPrice,
|
|
state: data.state,
|
|
symbol: where.dataSource_date_symbol.symbol
|
|
},
|
|
update: { marketPrice: data.marketPrice, state: data.state }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Upsert market data by imitating missing upsertMany functionality
|
|
* with $transaction
|
|
*/
|
|
public async updateMany({
|
|
data
|
|
}: {
|
|
data: Prisma.MarketDataUpdateInput[];
|
|
}): Promise<MarketData[]> {
|
|
const upsertPromises = data.map(
|
|
({ dataSource, date, marketPrice, symbol, state }) => {
|
|
return this.prismaService.marketData.upsert({
|
|
create: {
|
|
dataSource: <DataSource>dataSource,
|
|
date: <Date>date,
|
|
marketPrice: <number>marketPrice,
|
|
state: <MarketDataState>state,
|
|
symbol: <string>symbol
|
|
},
|
|
update: {
|
|
marketPrice: <number>marketPrice,
|
|
state: <MarketDataState>state
|
|
},
|
|
where: {
|
|
dataSource_date_symbol: {
|
|
dataSource: <DataSource>dataSource,
|
|
date: <Date>date,
|
|
symbol: <string>symbol
|
|
}
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
return this.prismaService.$transaction(upsertPromises);
|
|
}
|
|
}
|
|
|