Browse Source

Merge d3c2cdc4a9 into 80d1b5fc4e

pull/7672/merge
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
c910f4f188
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      CHANGELOG.md
  2. 7
      apps/api/src/app/import/import.service.ts
  3. 56
      apps/api/src/services/market-data/market-data.service.ts
  4. 10
      libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts
  5. 4
      libs/common/src/lib/dtos/index.ts
  6. 13
      libs/common/src/lib/dtos/market-data.dto.ts
  7. 9
      libs/common/src/lib/dtos/update-bulk-market-data.dto.ts
  8. 10
      libs/common/src/lib/dtos/update-market-data.dto.ts
  9. 2
      libs/common/src/lib/interfaces/index.ts
  10. 4
      libs/common/src/lib/interfaces/market-data.interface.ts
  11. 5
      libs/common/src/lib/interfaces/responses/export-response.interface.ts
  12. 4
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts

12
CHANGELOG.md

@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Hardened the validation of the market data in the activities import
- Hardened the validation of the market data in the historical market data import
### Fixed
- Fixed the date normalization of the market data in the activities import and in the historical market data import
- Fixed the date normalization of the market data for servers running in a time zone other than UTC
## 3.55.0 - 2026-08-19 ## 3.55.0 - 2026-08-19
### Added ### Added

7
apps/api/src/app/import/import.service.ts

@ -628,11 +628,12 @@ export class ImportService {
// Insert or update market data // Insert or update market data
const marketDataObjects = ( const marketDataObjects = (
assetProfileWithMarketData.marketData ?? [] assetProfileWithMarketData.marketData ?? []
).map((marketData) => { ).map(({ date, marketPrice }) => {
return { return {
...marketData, marketPrice,
symbol, symbol,
dataSource: assetProfileWithMarketData.dataSource dataSource: assetProfileWithMarketData.dataSource,
date: parseISO(date)
} as Prisma.MarketDataUpdateInput; } as Prisma.MarketDataUpdateInput;
}); });

56
apps/api/src/services/market-data/market-data.service.ts

@ -2,8 +2,7 @@ import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.i
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT } from '@ghostfolio/common/config'; import { DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT } from '@ghostfolio/common/config';
import { UpdateMarketDataDto } from '@ghostfolio/common/dtos'; import { getStartOfUtcDate } from '@ghostfolio/common/helper';
import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -36,7 +35,7 @@ export class MarketDataService {
where: { where: {
dataSource, dataSource,
symbol, symbol,
date: resetHours(date) date: getStartOfUtcDate(date)
} }
}); });
} }
@ -156,14 +155,24 @@ export class MarketDataService {
dataSource, dataSource,
symbol symbol
}: AssetProfileIdentifier & { data: Prisma.MarketDataUpdateInput[] }) { }: AssetProfileIdentifier & { data: Prisma.MarketDataUpdateInput[] }) {
const marketDataItems = data.map(({ date, marketPrice, state }) => {
return {
dataSource,
symbol,
date: getStartOfUtcDate(date as Date),
marketPrice: marketPrice as number,
state: state as MarketDataState
};
});
await this.prismaService.$transaction( await this.prismaService.$transaction(
async (prisma) => { async (prisma) => {
if (data.length > 0) { if (marketDataItems.length > 0) {
let minTime = Infinity; let minTime = Infinity;
let maxTime = -Infinity; let maxTime = -Infinity;
for (const { date } of data) { for (const { date } of marketDataItems) {
const time = (date as Date).getTime(); const time = date.getTime();
if (time < minTime) { if (time < minTime) {
minTime = time; minTime = time;
@ -189,13 +198,7 @@ export class MarketDataService {
}); });
await prisma.marketData.createMany({ await prisma.marketData.createMany({
data: data.map(({ date, marketPrice, state }) => ({ data: marketDataItems,
dataSource,
symbol,
date: date as Date,
marketPrice: marketPrice as number,
state: state as MarketDataState
})),
skipDuplicates: true skipDuplicates: true
}); });
} }
@ -220,27 +223,6 @@ export class MarketDataService {
}); });
} }
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 * Upsert market data by imitating missing upsertMany functionality
* with $transaction * with $transaction
@ -252,10 +234,12 @@ export class MarketDataService {
}): Promise<MarketData[]> { }): Promise<MarketData[]> {
const upsertPromises = data.map( const upsertPromises = data.map(
({ dataSource, date, marketPrice, symbol, state }) => { ({ dataSource, date, marketPrice, symbol, state }) => {
const dateOfMarketData = getStartOfUtcDate(date as Date);
return this.prismaService.marketData.upsert({ return this.prismaService.marketData.upsert({
create: { create: {
dataSource: dataSource as DataSource, dataSource: dataSource as DataSource,
date: date as Date, date: dateOfMarketData,
marketPrice: marketPrice as number, marketPrice: marketPrice as number,
state: state as MarketDataState, state: state as MarketDataState,
symbol: symbol as string symbol: symbol as string
@ -267,7 +251,7 @@ export class MarketDataService {
where: { where: {
dataSource_date_symbol: { dataSource_date_symbol: {
dataSource: dataSource as DataSource, dataSource: dataSource as DataSource,
date: date as Date, date: dateOfMarketData,
symbol: symbol as string symbol: symbol as string
} }
} }

10
libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts

@ -1,9 +1,9 @@
import { MarketData } from '@ghostfolio/common/interfaces';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { IsArray, IsIn, IsOptional } from 'class-validator'; import { Type } from 'class-transformer';
import { IsArray, IsIn, IsOptional, ValidateNested } from 'class-validator';
import { CreateAssetProfileDto } from './create-asset-profile.dto'; import { CreateAssetProfileDto } from './create-asset-profile.dto';
import { MarketDataDto } from './market-data.dto';
export class CreateAssetProfileWithMarketDataDto extends CreateAssetProfileDto { export class CreateAssetProfileWithMarketDataDto extends CreateAssetProfileDto {
@IsIn([DataSource.MANUAL], { @IsIn([DataSource.MANUAL], {
@ -13,5 +13,7 @@ export class CreateAssetProfileWithMarketDataDto extends CreateAssetProfileDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
marketData?: MarketData[]; @Type(() => MarketDataDto)
@ValidateNested({ each: true })
marketData?: MarketDataDto[];
} }

4
libs/common/src/lib/dtos/index.ts

@ -13,6 +13,7 @@ import { CreateTagDto } from './create-tag.dto';
import { CreateWatchlistItemDto } from './create-watchlist-item.dto'; import { CreateWatchlistItemDto } from './create-watchlist-item.dto';
import { DeleteOwnUserDto } from './delete-own-user.dto'; import { DeleteOwnUserDto } from './delete-own-user.dto';
import { HoldingDto } from './holding.dto'; import { HoldingDto } from './holding.dto';
import { MarketDataDto } from './market-data.dto';
import { MergeAssetProfileDto } from './merge-asset-profile.dto'; import { MergeAssetProfileDto } from './merge-asset-profile.dto';
import { ScraperConfigurationDto } from './scraper-configuration.dto'; import { ScraperConfigurationDto } from './scraper-configuration.dto';
import { SectorDto } from './sector.dto'; import { SectorDto } from './sector.dto';
@ -22,7 +23,6 @@ import { UpdateAccountDto } from './update-account.dto';
import { UpdateAssetProfileDataDto } from './update-asset-profile-data.dto'; import { UpdateAssetProfileDataDto } from './update-asset-profile-data.dto';
import { UpdateAssetProfileDto } from './update-asset-profile.dto'; import { UpdateAssetProfileDto } from './update-asset-profile.dto';
import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto'; import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto';
import { UpdateMarketDataDto } from './update-market-data.dto';
import { UpdateOrderDto } from './update-order.dto'; import { UpdateOrderDto } from './update-order.dto';
import { UpdateOwnAccessTokenDto } from './update-own-access-token.dto'; import { UpdateOwnAccessTokenDto } from './update-own-access-token.dto';
import { UpdatePlatformDto } from './update-platform.dto'; import { UpdatePlatformDto } from './update-platform.dto';
@ -46,6 +46,7 @@ export {
CreateWatchlistItemDto, CreateWatchlistItemDto,
DeleteOwnUserDto, DeleteOwnUserDto,
HoldingDto, HoldingDto,
MarketDataDto,
MergeAssetProfileDto, MergeAssetProfileDto,
ScraperConfigurationDto, ScraperConfigurationDto,
SectorDto, SectorDto,
@ -55,7 +56,6 @@ export {
UpdateAssetProfileDataDto, UpdateAssetProfileDataDto,
UpdateAssetProfileDto, UpdateAssetProfileDto,
UpdateBulkMarketDataDto, UpdateBulkMarketDataDto,
UpdateMarketDataDto,
UpdateOrderDto, UpdateOrderDto,
UpdateOwnAccessTokenDto, UpdateOwnAccessTokenDto,
UpdatePlatformDto, UpdatePlatformDto,

13
libs/common/src/lib/dtos/market-data.dto.ts

@ -0,0 +1,13 @@
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
import { IsISO8601, IsNumber, Min, Validate } from 'class-validator';
export class MarketDataDto {
@IsISO8601({ strict: true, strictSeparator: true })
@Validate(IsAfter1970Constraint)
date: string;
@IsNumber()
@Min(0)
marketPrice: number;
}

9
libs/common/src/lib/dtos/update-bulk-market-data.dto.ts

@ -1,11 +1,12 @@
import { UpdateMarketDataDto } from '@ghostfolio/common/dtos'; import { MarketDataDto } from '@ghostfolio/common/dtos';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray } from 'class-validator'; import { ArrayNotEmpty, IsArray, ValidateNested } from 'class-validator';
export class UpdateBulkMarketDataDto { export class UpdateBulkMarketDataDto {
@ArrayNotEmpty() @ArrayNotEmpty()
@IsArray() @IsArray()
@Type(() => UpdateMarketDataDto) @Type(() => MarketDataDto)
marketData: UpdateMarketDataDto[]; @ValidateNested({ each: true })
marketData: MarketDataDto[];
} }

10
libs/common/src/lib/dtos/update-market-data.dto.ts

@ -1,10 +0,0 @@
import { IsISO8601, IsNumber, IsOptional } from 'class-validator';
export class UpdateMarketDataDto {
@IsISO8601()
@IsOptional()
date?: string;
@IsNumber()
marketPrice: number;
}

2
libs/common/src/lib/interfaces/index.ts

@ -28,7 +28,6 @@ import type {
NullableLineChartItem NullableLineChartItem
} from './line-chart-item.interface'; } from './line-chart-item.interface';
import type { LookupItem } from './lookup-item.interface'; import type { LookupItem } from './lookup-item.interface';
import type { MarketData } from './market-data.interface';
import type { PortfolioChart } from './portfolio-chart.interface'; import type { PortfolioChart } from './portfolio-chart.interface';
import type { PortfolioDetails } from './portfolio-details.interface'; import type { PortfolioDetails } from './portfolio-details.interface';
import type { PortfolioPerformance } from './portfolio-performance.interface'; import type { PortfolioPerformance } from './portfolio-performance.interface';
@ -159,7 +158,6 @@ export {
LineChartItem, LineChartItem,
LookupItem, LookupItem,
LookupResponse, LookupResponse,
MarketData,
MarketDataOfMarketsResponse, MarketDataOfMarketsResponse,
NullableLineChartItem, NullableLineChartItem,
OAuthResponse, OAuthResponse,

4
libs/common/src/lib/interfaces/market-data.interface.ts

@ -1,4 +0,0 @@
export interface MarketData {
date: string;
marketPrice: number;
}

5
libs/common/src/lib/interfaces/responses/export-response.interface.ts

@ -1,8 +1,9 @@
import { MarketDataDto } from '@ghostfolio/common/dtos';
import { Account, Order, Platform, SymbolProfile, Tag } from '@prisma/client'; import { Account, Order, Platform, SymbolProfile, Tag } from '@prisma/client';
import { AccountBalance } from '../account-balance.interface'; import { AccountBalance } from '../account-balance.interface';
import { AssetProfileIdentifier } from '../asset-profile-identifier.interface'; import { AssetProfileIdentifier } from '../asset-profile-identifier.interface';
import { MarketData } from '../market-data.interface';
import { UserSettings } from '../user-settings.interface'; import { UserSettings } from '../user-settings.interface';
export interface ExportResponse { export interface ExportResponse {
@ -29,7 +30,7 @@ export interface ExportResponse {
| 'updatedAt' | 'updatedAt'
| 'userId' | 'userId'
> & { > & {
marketData: MarketData[]; marketData: MarketDataDto[];
})[]; })[];
meta: { meta: {
date: string; date: string;

4
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts

@ -1,4 +1,4 @@
import { UpdateMarketDataDto } from '@ghostfolio/common/dtos'; import { MarketDataDto } from '@ghostfolio/common/dtos';
import { import {
DATE_FORMAT, DATE_FORMAT,
getDateFormatString, getDateFormatString,
@ -249,7 +249,7 @@ export class GfHistoricalMarketDataEditorComponent
public onImportHistoricalData() { public onImportHistoricalData() {
try { try {
const marketData = csvToJson<UpdateMarketDataDto>( const marketData = csvToJson<MarketDataDto>(
this.historicalDataForm.controls.historicalData.controls.csvString this.historicalDataForm.controls.historicalData.controls.csvString
.value ?? '', .value ?? '',
{ {

Loading…
Cancel
Save