From 4f76ee67588ba5ba8b1d3ca15e90745170fd029d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Tom=C3=A1s=20Fern=C3=A1ndez=20Mart=C3=ADn?= Date: Sat, 15 Feb 2025 09:43:28 +0100 Subject: [PATCH 1/8] Feature/improve symbol lookup in Trackinsight data enhancer (#4296) * Feature: enhance Trackinsight data fetching with symbol search fallback * Feature: refactor Trackinsight symbol search for improved clarity and error handling * Fixed style concerns and removed code duplication * Minor improvements * Update changelog * Update changelog * Clean up --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 1 + .../trackinsight/trackinsight.service.ts | 83 +++++++++++-------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37bc84cf..db1563f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the symbol lookup in the _Trackinsight_ data enhancer for asset profile data - Improved the language localization for German (`de`) - Upgraded `@trivago/prettier-plugin-sort-imports` from version `5.2.1` to `5.2.2` diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts index 56b90082..2b8eaf2e 100644 --- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts @@ -44,28 +44,31 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { return response; } + let trackinsightSymbol = await this.searchTrackinsightSymbol({ + requestTimeout, + symbol + }); + + if (!trackinsightSymbol) { + trackinsightSymbol = await this.searchTrackinsightSymbol({ + requestTimeout, + symbol: symbol.split('.')?.[0] + }); + } + + if (!trackinsightSymbol) { + return response; + } + const profile = await fetch( - `${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`, + `${TrackinsightDataEnhancerService.baseUrl}/funds/${trackinsightSymbol}.json`, { signal: AbortSignal.timeout(requestTimeout) } ) .then((res) => res.json()) .catch(() => { - return fetch( - `${TrackinsightDataEnhancerService.baseUrl}/funds/${ - symbol.split('.')?.[0] - }.json`, - { - signal: AbortSignal.timeout( - this.configurationService.get('REQUEST_TIMEOUT') - ) - } - ) - .then((res) => res.json()) - .catch(() => { - return {}; - }); + return {}; }); const isin = profile?.isin?.split(';')?.[0]; @@ -75,29 +78,14 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { } const holdings = await fetch( - `${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`, + `${TrackinsightDataEnhancerService.baseUrl}/holdings/${trackinsightSymbol}.json`, { - signal: AbortSignal.timeout( - this.configurationService.get('REQUEST_TIMEOUT') - ) + signal: AbortSignal.timeout(requestTimeout) } ) .then((res) => res.json()) .catch(() => { - return fetch( - `${TrackinsightDataEnhancerService.baseUrl}/holdings/${ - symbol.split('.')?.[0] - }.json`, - { - signal: AbortSignal.timeout( - this.configurationService.get('REQUEST_TIMEOUT') - ) - } - ) - .then((res) => res.json()) - .catch(() => { - return {}; - }); + return {}; }); if ( @@ -180,4 +168,33 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { public getTestSymbol() { return 'QQQ'; } + + private async searchTrackinsightSymbol({ + requestTimeout, + symbol + }: { + requestTimeout: number; + symbol: string; + }) { + return fetch( + `https://www.trackinsight.com/search-api/search_v2/${symbol}/_/ticker/default/0/3`, + { + signal: AbortSignal.timeout(requestTimeout) + } + ) + .then((res) => res.json()) + .then((jsonRes) => { + if ( + jsonRes['results']?.['count'] === 1 || + jsonRes['results']?.['docs']?.[0]?.['ticker'] === symbol + ) { + return jsonRes['results']['docs'][0]['ticker']; + } + + return undefined; + }) + .catch(() => { + return undefined; + }); + } } From f1ab3ff8e85530106a9e904907ea36a5c5f5cf7c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 15 Feb 2025 10:00:14 +0100 Subject: [PATCH 2/8] Feature/modernize tags endpoint (#4317) * Modernize tags endpoint * Update changelog --- CHANGELOG.md | 2 +- apps/api/src/app/app.module.ts | 4 +- .../{tag => endpoints/tags}/create-tag.dto.ts | 0 .../tags/tags.controller.ts} | 56 ++++++------- .../api/src/app/endpoints/tags/tags.module.ts | 12 +++ .../{tag => endpoints/tags}/update-tag.dto.ts | 0 apps/api/src/app/tag/tag.module.ts | 14 ---- apps/api/src/app/tag/tag.service.ts | 81 ------------------- apps/api/src/services/tag/tag.service.ts | 73 +++++++++++++++++ .../admin-tag/admin-tag.component.ts | 14 ++-- .../create-or-update-tag-dialog.component.ts | 4 +- .../holding-detail-dialog.component.ts | 4 +- .../holding-detail-dialog.html | 4 +- apps/client/src/app/services/admin.service.ts | 20 +---- apps/client/src/app/services/data.service.ts | 18 +++++ 15 files changed, 147 insertions(+), 159 deletions(-) rename apps/api/src/app/{tag => endpoints/tags}/create-tag.dto.ts (100%) rename apps/api/src/app/{tag/tag.controller.ts => endpoints/tags/tags.controller.ts} (95%) create mode 100644 apps/api/src/app/endpoints/tags/tags.module.ts rename apps/api/src/app/{tag => endpoints/tags}/update-tag.dto.ts (100%) delete mode 100644 apps/api/src/app/tag/tag.module.ts delete mode 100644 apps/api/src/app/tag/tag.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index db1563f2..c5e98236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Asia-Pacific Markets) - Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Japan) -- Added support to create custom tags in the holding detail dialog +- Added support to create custom tags in the holding detail dialog (experimental) - Extended the tags selector component by a `readonly` attribute - Extended the tags selector component to support creating custom tags - Extended the holding detail dialog by the historical market data editor (experimental) diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 6d097aef..3e68bf42 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -36,6 +36,7 @@ import { ApiKeysModule } from './endpoints/api-keys/api-keys.module'; import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; import { MarketDataModule } from './endpoints/market-data/market-data.module'; import { PublicModule } from './endpoints/public/public.module'; +import { TagsModule } from './endpoints/tags/tags.module'; import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { ExportModule } from './export/export.module'; import { HealthModule } from './health/health.module'; @@ -49,7 +50,6 @@ import { RedisCacheModule } from './redis-cache/redis-cache.module'; import { SitemapModule } from './sitemap/sitemap.module'; import { SubscriptionModule } from './subscription/subscription.module'; import { SymbolModule } from './symbol/symbol.module'; -import { TagModule } from './tag/tag.module'; import { UserModule } from './user/user.module'; @Module({ @@ -124,7 +124,7 @@ import { UserModule } from './user/user.module'; SitemapModule, SubscriptionModule, SymbolModule, - TagModule, + TagsModule, TwitterBotModule, UserModule ], diff --git a/apps/api/src/app/tag/create-tag.dto.ts b/apps/api/src/app/endpoints/tags/create-tag.dto.ts similarity index 100% rename from apps/api/src/app/tag/create-tag.dto.ts rename to apps/api/src/app/endpoints/tags/create-tag.dto.ts diff --git a/apps/api/src/app/tag/tag.controller.ts b/apps/api/src/app/endpoints/tags/tags.controller.ts similarity index 95% rename from apps/api/src/app/tag/tag.controller.ts rename to apps/api/src/app/endpoints/tags/tags.controller.ts index 862f0a70..bf216bb2 100644 --- a/apps/api/src/app/tag/tag.controller.ts +++ b/apps/api/src/app/endpoints/tags/tags.controller.ts @@ -1,5 +1,6 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { RequestWithUser } from '@ghostfolio/common/types'; @@ -21,23 +22,15 @@ import { Tag } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { CreateTagDto } from './create-tag.dto'; -import { TagService } from './tag.service'; import { UpdateTagDto } from './update-tag.dto'; -@Controller('tag') -export class TagController { +@Controller('tags') +export class TagsController { public constructor( @Inject(REQUEST) private readonly request: RequestWithUser, private readonly tagService: TagService ) {} - @Get() - @HasPermission(permissions.readTags) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async getTags() { - return this.tagService.getTagsWithActivityCount(); - } - @Post() @UseGuards(AuthGuard('jwt')) public async createTag(@Body() data: CreateTagDto): Promise { @@ -70,6 +63,31 @@ export class TagController { return this.tagService.createTag(data); } + @Delete(':id') + @HasPermission(permissions.deleteTag) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async deleteTag(@Param('id') id: string) { + const originalTag = await this.tagService.getTag({ + id + }); + + if (!originalTag) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return this.tagService.deleteTag({ id }); + } + + @Get() + @HasPermission(permissions.readTags) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async getTags() { + return this.tagService.getTagsWithActivityCount(); + } + @HasPermission(permissions.updateTag) @Put(':id') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @@ -94,22 +112,4 @@ export class TagController { } }); } - - @Delete(':id') - @HasPermission(permissions.deleteTag) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async deleteTag(@Param('id') id: string) { - const originalTag = await this.tagService.getTag({ - id - }); - - if (!originalTag) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - - return this.tagService.deleteTag({ id }); - } } diff --git a/apps/api/src/app/endpoints/tags/tags.module.ts b/apps/api/src/app/endpoints/tags/tags.module.ts new file mode 100644 index 00000000..a8a2f1c5 --- /dev/null +++ b/apps/api/src/app/endpoints/tags/tags.module.ts @@ -0,0 +1,12 @@ +import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; + +import { Module } from '@nestjs/common'; + +import { TagsController } from './tags.controller'; + +@Module({ + controllers: [TagsController], + imports: [PrismaModule, TagModule] +}) +export class TagsModule {} diff --git a/apps/api/src/app/tag/update-tag.dto.ts b/apps/api/src/app/endpoints/tags/update-tag.dto.ts similarity index 100% rename from apps/api/src/app/tag/update-tag.dto.ts rename to apps/api/src/app/endpoints/tags/update-tag.dto.ts diff --git a/apps/api/src/app/tag/tag.module.ts b/apps/api/src/app/tag/tag.module.ts deleted file mode 100644 index 48587c54..00000000 --- a/apps/api/src/app/tag/tag.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; - -import { Module } from '@nestjs/common'; - -import { TagController } from './tag.controller'; -import { TagService } from './tag.service'; - -@Module({ - controllers: [TagController], - exports: [TagService], - imports: [PrismaModule], - providers: [TagService] -}) -export class TagModule {} diff --git a/apps/api/src/app/tag/tag.service.ts b/apps/api/src/app/tag/tag.service.ts deleted file mode 100644 index c4a5447a..00000000 --- a/apps/api/src/app/tag/tag.service.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; - -import { Injectable } from '@nestjs/common'; -import { Prisma, Tag } from '@prisma/client'; - -@Injectable() -export class TagService { - public constructor(private readonly prismaService: PrismaService) {} - - public async createTag(data: Prisma.TagCreateInput) { - return this.prismaService.tag.create({ - data - }); - } - - public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise { - return this.prismaService.tag.delete({ where }); - } - - public async getTag( - tagWhereUniqueInput: Prisma.TagWhereUniqueInput - ): Promise { - return this.prismaService.tag.findUnique({ - where: tagWhereUniqueInput - }); - } - - public async getTags({ - cursor, - orderBy, - skip, - take, - where - }: { - cursor?: Prisma.TagWhereUniqueInput; - orderBy?: Prisma.TagOrderByWithRelationInput; - skip?: number; - take?: number; - where?: Prisma.TagWhereInput; - } = {}) { - return this.prismaService.tag.findMany({ - cursor, - orderBy, - skip, - take, - where - }); - } - - public async getTagsWithActivityCount() { - const tagsWithOrderCount = await this.prismaService.tag.findMany({ - include: { - _count: { - select: { orders: true } - } - } - }); - - return tagsWithOrderCount.map(({ _count, id, name, userId }) => { - return { - id, - name, - userId, - activityCount: _count.orders - }; - }); - } - - public async updateTag({ - data, - where - }: { - data: Prisma.TagUpdateInput; - where: Prisma.TagWhereUniqueInput; - }): Promise { - return this.prismaService.tag.update({ - data, - where - }); - } -} diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index b16f22fb..3d6bd390 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -1,11 +1,52 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { Injectable } from '@nestjs/common'; +import { Prisma, Tag } from '@prisma/client'; @Injectable() export class TagService { public constructor(private readonly prismaService: PrismaService) {} + public async createTag(data: Prisma.TagCreateInput) { + return this.prismaService.tag.create({ + data + }); + } + + public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise { + return this.prismaService.tag.delete({ where }); + } + + public async getTag( + tagWhereUniqueInput: Prisma.TagWhereUniqueInput + ): Promise { + return this.prismaService.tag.findUnique({ + where: tagWhereUniqueInput + }); + } + + public async getTags({ + cursor, + orderBy, + skip, + take, + where + }: { + cursor?: Prisma.TagWhereUniqueInput; + orderBy?: Prisma.TagOrderByWithRelationInput; + skip?: number; + take?: number; + where?: Prisma.TagWhereInput; + } = {}) { + return this.prismaService.tag.findMany({ + cursor, + orderBy, + skip, + take, + where + }); + } + public async getTagsForUser(userId: string) { const tags = await this.prismaService.tag.findMany({ include: { @@ -41,4 +82,36 @@ export class TagService { isUsed: _count.orders > 0 })); } + + public async getTagsWithActivityCount() { + const tagsWithOrderCount = await this.prismaService.tag.findMany({ + include: { + _count: { + select: { orders: true } + } + } + }); + + return tagsWithOrderCount.map(({ _count, id, name, userId }) => { + return { + id, + name, + userId, + activityCount: _count.orders + }; + }); + } + + public async updateTag({ + data, + where + }: { + data: Prisma.TagUpdateInput; + where: Prisma.TagWhereUniqueInput; + }): Promise { + return this.prismaService.tag.update({ + data, + where + }); + } } diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.ts b/apps/client/src/app/components/admin-tag/admin-tag.component.ts index 3c0bdb68..bd7432b3 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.ts +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.ts @@ -1,8 +1,7 @@ -import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; -import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; +import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto'; +import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto'; import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; -import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; @@ -43,7 +42,6 @@ export class AdminTagComponent implements OnInit, OnDestroy { private unsubscribeSubject = new Subject(); public constructor( - private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private deviceService: DeviceDetectorService, @@ -100,7 +98,7 @@ export class AdminTagComponent implements OnInit, OnDestroy { } private deleteTag(aId: string) { - this.adminService + this.dataService .deleteTag(aId) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe({ @@ -116,7 +114,7 @@ export class AdminTagComponent implements OnInit, OnDestroy { } private fetchTags() { - this.adminService + this.dataService .fetchTags() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((tags) => { @@ -148,7 +146,7 @@ export class AdminTagComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((tag: CreateTagDto | null) => { if (tag) { - this.adminService + this.dataService .postTag(tag) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe({ @@ -184,7 +182,7 @@ export class AdminTagComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((tag: UpdateTagDto | null) => { if (tag) { - this.adminService + this.dataService .putTag(tag) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe({ diff --git a/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts b/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts index 9b7194cc..1bbda8e1 100644 --- a/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts +++ b/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts @@ -1,5 +1,5 @@ -import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; -import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; +import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto'; +import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto'; import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 54fab34d..69322b21 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -2,7 +2,6 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; -import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { NUMERICAL_PRECISION_THRESHOLD } from '@ghostfolio/common/config'; @@ -133,7 +132,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { private unsubscribeSubject = new Subject(); public constructor( - private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, public dialogRef: MatDialogRef, @@ -162,7 +160,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { }); if (newTag && this.hasPermissionToCreateOwnTag) { - this.adminService + this.dataService .postTag({ ...newTag, userId: this.user.id }) .pipe( switchMap((createdTag) => { diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index df1c0c62..a767a579 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -388,7 +388,9 @@ (`/api/v1/tag/${aId}`); - } - public executeJob(aId: string) { return this.http.get(`/api/v1/admin/queue/job/${aId}/execute`); } @@ -155,10 +149,6 @@ export class AdminService { return this.http.get('/api/v1/platform'); } - public fetchTags() { - return this.http.get('/api/v1/tag'); - } - public fetchUsers({ skip, take = DEFAULT_PAGE_SIZE @@ -261,10 +251,6 @@ export class AdminService { return this.http.post(`/api/v1/platform`, aPlatform); } - public postTag(aTag: CreateTagDto) { - return this.http.post(`/api/v1/tag`, aTag); - } - public putPlatform(aPlatform: UpdatePlatformDto) { return this.http.put( `/api/v1/platform/${aPlatform.id}`, @@ -272,10 +258,6 @@ export class AdminService { ); } - public putTag(aTag: UpdateTagDto) { - return this.http.put(`/api/v1/tag/${aTag.id}`, aTag); - } - public testMarketData({ dataSource, scraperConfiguration, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 4a57d587..0da1275e 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -4,6 +4,8 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto'; +import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto'; +import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activities, @@ -308,6 +310,10 @@ export class DataService { return this.http.delete(`/api/v1/user`, { body: aData }); } + public deleteTag(aId: string) { + return this.http.delete(`/api/v1/tags/${aId}`); + } + public deleteUser(aId: string) { return this.http.delete(`/api/v1/user/${aId}`); } @@ -662,6 +668,10 @@ export class DataService { ); } + public fetchTags() { + return this.http.get('/api/v1/tags'); + } + public loginAnonymous(accessToken: string) { return this.http.post('/api/v1/auth/anonymous', { accessToken @@ -709,6 +719,10 @@ export class DataService { return this.http.post('/api/v1/order', aOrder); } + public postTag(aTag: CreateTagDto) { + return this.http.post(`/api/v1/tags`, aTag); + } + public postUser() { return this.http.post('/api/v1/user', {}); } @@ -736,6 +750,10 @@ export class DataService { return this.http.put(`/api/v1/order/${aOrder.id}`, aOrder); } + public putTag(aTag: UpdateTagDto) { + return this.http.put(`/api/v1/tags/${aTag.id}`, aTag); + } + public putUserSetting(aData: UpdateUserSettingDto) { return this.http.put('/api/v1/user/setting', aData); } From b8c6a73f30bea84a120144eaf2c388250e91fe62 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 15 Feb 2025 10:41:15 +0100 Subject: [PATCH 3/8] Feature/update locales 20250215 (#4314) * Update translation --------- Co-authored-by: github-actions[bot] Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- apps/client/src/locales/messages.ca.xlf | 14 +++++++++++--- apps/client/src/locales/messages.de.xlf | 14 +++++++++++--- apps/client/src/locales/messages.es.xlf | 14 +++++++++++--- apps/client/src/locales/messages.fr.xlf | 14 +++++++++++--- apps/client/src/locales/messages.it.xlf | 14 +++++++++++--- apps/client/src/locales/messages.nl.xlf | 14 +++++++++++--- apps/client/src/locales/messages.pl.xlf | 14 +++++++++++--- apps/client/src/locales/messages.pt.xlf | 14 +++++++++++--- apps/client/src/locales/messages.tr.xlf | 14 +++++++++++--- apps/client/src/locales/messages.uk.xlf | 14 +++++++++++--- apps/client/src/locales/messages.xlf | 13 ++++++++++--- apps/client/src/locales/messages.zh.xlf | 14 +++++++++++--- 12 files changed, 131 insertions(+), 36 deletions(-) diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index 82f3f96b..d70c773e 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -1787,7 +1787,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -2239,7 +2239,7 @@ Està segur que vol eliminar aquesta etiqueta? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -2639,7 +2639,7 @@ Informar d’un Problema amb les Dades apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -7756,6 +7756,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 0040f8cb..b7ff0064 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -1370,7 +1370,7 @@ Datenfehler melden apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -3370,7 +3370,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -5891,7 +5891,7 @@ Möchtest du diesen Tag wirklich löschen? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -7756,6 +7756,14 @@ 50 + + Market Data + Marktdaten + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index bc6a6241..e1241586 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -1371,7 +1371,7 @@ Reporta un anomalía de los datos apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -3371,7 +3371,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -5892,7 +5892,7 @@ Do you really want to delete this tag? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -7757,6 +7757,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index 353a2a3f..bc5d9443 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -1718,7 +1718,7 @@ Signaler une Erreur de Données apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -2598,7 +2598,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -5891,7 +5891,7 @@ Confirmez la suppression de ce tag ? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -7756,6 +7756,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index c6cb7ac7..f6761f2c 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -1371,7 +1371,7 @@ Segnala un’anomalia dei dati apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -3371,7 +3371,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -5892,7 +5892,7 @@ Sei sicuro di voler eliminare questo tag? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -7757,6 +7757,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index 53efeab9..aab3a393 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -1370,7 +1370,7 @@ Gegevensstoring melden apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -3370,7 +3370,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -5891,7 +5891,7 @@ Do you really want to delete this tag? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -7756,6 +7756,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index 2942a489..e2e157e1 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -1639,7 +1639,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -2067,7 +2067,7 @@ Czy naprawdę chcesz usunąć ten tag? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -2803,7 +2803,7 @@ Zgłoś Błąd Danych apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -7756,6 +7756,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 8b9b7ac0..69af89c1 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -1678,7 +1678,7 @@ Dados do Relatório com Problema apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -3378,7 +3378,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -5891,7 +5891,7 @@ Do you really want to delete this tag? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -7756,6 +7756,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 3c7d3f10..c1e9963f 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -2647,7 +2647,7 @@ Rapor Veri Sorunu apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -3955,7 +3955,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -5891,7 +5891,7 @@ Bu etiketi silmeyi gerçekten istiyor musunuz? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -7756,6 +7756,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.uk.xlf b/apps/client/src/locales/messages.uk.xlf index 26690077..f582408b 100644 --- a/apps/client/src/locales/messages.uk.xlf +++ b/apps/client/src/locales/messages.uk.xlf @@ -2363,7 +2363,7 @@ Ви дійсно хочете видалити цей тег? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -2767,7 +2767,7 @@ Повідомити про збій даних apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -5139,7 +5139,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -7756,6 +7756,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index ac05bc01..eba1176e 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -1569,7 +1569,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -1962,7 +1962,7 @@ Do you really want to delete this tag? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -2623,7 +2623,7 @@ Report Data Glitch apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -7014,6 +7014,13 @@ 50 + + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index 68090697..16e86c2f 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -1648,7 +1648,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 70 + 71 @@ -2084,7 +2084,7 @@ 您真的要删除此标签吗? apps/client/src/app/components/admin-tag/admin-tag.component.ts - 87 + 85 @@ -2820,7 +2820,7 @@ 报告数据故障 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 385 + 409 @@ -7757,6 +7757,14 @@ 50 + + Market Data + Market Data + + apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html + 374 + + From ab9133fa2478128f32472e0d3fdf267c14594762 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 15 Feb 2025 10:53:06 +0100 Subject: [PATCH 4/8] Bugfix/fix gaps in chart of benchmark comparator (#4311) * Fix benchmark interval * Update changelog --- CHANGELOG.md | 4 ++ .../src/app/benchmark/benchmark.controller.ts | 30 ++++++++- .../api/src/app/benchmark/benchmark.module.ts | 30 ++++++++- .../src/app/benchmark/benchmark.service.ts | 61 ++++++++++--------- .../analysis/analysis-page.component.ts | 1 + apps/client/src/app/services/data.service.ts | 14 +++-- 6 files changed, 101 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e98236..4a24fb60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the language localization for German (`de`) - Upgraded `@trivago/prettier-plugin-sort-imports` from version `5.2.1` to `5.2.2` +### Fixed + +- Fixed the gaps in the chart of the benchmark comparator + ## 2.138.0 - 2025-02-08 ### Added diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/benchmark/benchmark.controller.ts index 66c268b9..d1908101 100644 --- a/apps/api/src/app/benchmark/benchmark.controller.ts +++ b/apps/api/src/app/benchmark/benchmark.controller.ts @@ -2,7 +2,9 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; +import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import type { AssetProfileIdentifier, BenchmarkMarketDataDetails, @@ -16,6 +18,7 @@ import { Controller, Delete, Get, + Headers, HttpException, Inject, Param, @@ -34,6 +37,7 @@ import { BenchmarkService } from './benchmark.service'; @Controller('benchmark') export class BenchmarkController { public constructor( + private readonly apiService: ApiService, private readonly benchmarkService: BenchmarkService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -108,23 +112,43 @@ export class BenchmarkController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getBenchmarkMarketDataForUser( + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Param('dataSource') dataSource: DataSource, @Param('startDateString') startDateString: string, @Param('symbol') symbol: string, - @Query('range') dateRange: DateRange = 'max' + @Query('range') dateRange: DateRange = 'max', + @Query('accounts') filterByAccounts?: string, + @Query('assetClasses') filterByAssetClasses?: string, + @Query('dataSource') filterByDataSource?: string, + @Query('symbol') filterBySymbol?: string, + @Query('tags') filterByTags?: string, + @Query('withExcludedAccounts') withExcludedAccountsParam = 'false' ): Promise { const { endDate, startDate } = getIntervalFromDateRange( dateRange, new Date(startDateString) ); - const userCurrency = this.request.user.Settings.settings.baseCurrency; + + const filters = this.apiService.buildFiltersFromQueryParams({ + filterByAccounts, + filterByAssetClasses, + filterByDataSource, + filterBySymbol, + filterByTags + }); + + const withExcludedAccounts = withExcludedAccountsParam === 'true'; return this.benchmarkService.getMarketDataForUser({ dataSource, + dateRange, endDate, + filters, + impersonationId, startDate, symbol, - userCurrency + withExcludedAccounts, + user: this.request.user }); } } diff --git a/apps/api/src/app/benchmark/benchmark.module.ts b/apps/api/src/app/benchmark/benchmark.module.ts index 4c5f4d58..8c042872 100644 --- a/apps/api/src/app/benchmark/benchmark.module.ts +++ b/apps/api/src/app/benchmark/benchmark.module.ts @@ -1,13 +1,25 @@ +import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; +import { AccountService } from '@ghostfolio/api/app/account/account.service'; +import { OrderModule } from '@ghostfolio/api/app/order/order.module'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; +import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; +import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; +import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; +import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { Module } from '@nestjs/common'; @@ -19,18 +31,32 @@ import { BenchmarkService } from './benchmark.service'; controllers: [BenchmarkController], exports: [BenchmarkService], imports: [ + ApiModule, ConfigurationModule, DataProviderModule, ExchangeRateDataModule, + ImpersonationModule, MarketDataModule, + OrderModule, + PortfolioSnapshotQueueModule, PrismaModule, PropertyModule, RedisCacheModule, SymbolModule, SymbolProfileModule, TransformDataSourceInRequestModule, - TransformDataSourceInResponseModule + TransformDataSourceInResponseModule, + UserModule ], - providers: [BenchmarkService] + providers: [ + AccountBalanceService, + AccountService, + BenchmarkService, + CurrentRateService, + MarketDataService, + PortfolioCalculatorFactory, + PortfolioService, + RulesService + ] }) export class BenchmarkModule {} diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index 4e466668..3efbc74f 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -1,6 +1,6 @@ +import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; @@ -22,22 +22,19 @@ import { Benchmark, BenchmarkMarketDataDetails, BenchmarkProperty, - BenchmarkResponse + BenchmarkResponse, + Filter } from '@ghostfolio/common/interfaces'; -import { BenchmarkTrend } from '@ghostfolio/common/types'; +import { + BenchmarkTrend, + DateRange, + UserWithSettings +} from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; -import { - addHours, - differenceInDays, - eachDayOfInterval, - format, - isAfter, - isSameDay, - subDays -} from 'date-fns'; +import { addHours, format, isAfter, isSameDay, subDays } from 'date-fns'; import { isNumber, uniqBy } from 'lodash'; import ms from 'ms'; @@ -48,11 +45,11 @@ export class BenchmarkService { private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS'; public constructor( - private readonly configurationService: ConfigurationService, private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, + private readonly portfolioService: PortfolioService, private readonly propertyService: PropertyService, private readonly redisCacheService: RedisCacheService, private readonly symbolProfileService: SymbolProfileService, @@ -158,31 +155,33 @@ export class BenchmarkService { public async getMarketDataForUser({ dataSource, + dateRange, endDate = new Date(), + filters, + impersonationId, startDate, symbol, - userCurrency + user, + withExcludedAccounts }: { + dateRange: DateRange; endDate?: Date; + filters?: Filter[]; + impersonationId: string; startDate: Date; - userCurrency: string; + user: UserWithSettings; + withExcludedAccounts?: boolean; } & AssetProfileIdentifier): Promise { const marketData: { date: string; value: number }[] = []; + const userCurrency = user.Settings.settings.baseCurrency; + const userId = user.id; - const days = differenceInDays(endDate, startDate) + 1; - const dates = eachDayOfInterval( - { - start: startDate, - end: endDate - }, - { - step: Math.round( - days / - Math.min(days, this.configurationService.get('MAX_CHART_ITEMS')) - ) - } - ).map((date) => { - return resetHours(date); + const { chart } = await this.portfolioService.getPerformance({ + dateRange, + filters, + impersonationId, + userId, + withExcludedAccounts }); const [currentSymbolItem, marketDataItems] = await Promise.all([ @@ -200,7 +199,9 @@ export class BenchmarkService { dataSource, symbol, date: { - in: dates + in: chart.map(({ date }) => { + return resetHours(parseDate(date)); + }) } } }) diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index a9a189d1..5eebb42e 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -321,6 +321,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { .fetchBenchmarkForUser({ dataSource, symbol, + filters: this.userService.getFilters(), range: this.user?.settings?.dateRange, startDate: this.firstOrderDate }) diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 0da1275e..6782644e 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -338,17 +338,23 @@ export class DataService { public fetchBenchmarkForUser({ dataSource, + filters, range, startDate, - symbol + symbol, + withExcludedAccounts }: { + filters?: Filter[]; range: DateRange; startDate: Date; + withExcludedAccounts?: boolean; } & AssetProfileIdentifier): Observable { - let params = new HttpParams(); + let params = this.buildFiltersAsQueryParams({ filters }); - if (range) { - params = params.append('range', range); + params = params.append('range', range); + + if (withExcludedAccounts) { + params = params.append('withExcludedAccounts', withExcludedAccounts); } return this.http.get( From 239adc1045104053df33155836925f540b86905b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 15 Feb 2025 10:55:37 +0100 Subject: [PATCH 5/8] Release 2.139.0 (#4320) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a24fb60..4b5be634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.139.0 - 2025-02-15 ### Added diff --git a/package-lock.json b/package-lock.json index e015f580..430ab9fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.138.0", + "version": "2.139.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.138.0", + "version": "2.139.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index e3043de1..587e88ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.138.0", + "version": "2.139.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From e26b015407b774a2dfc92e9db0d270258c56058b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:50:41 +0100 Subject: [PATCH 6/8] Bugfix/cannot register cron jobs (#4325) * Reorganize benchmark modules and move benchmarks endpoint --- apps/api/src/app/admin/admin.module.ts | 2 +- apps/api/src/app/admin/admin.service.ts | 2 +- apps/api/src/app/app.module.ts | 4 +- .../benchmarks/benchmarks.controller.ts} | 10 +- .../benchmarks/benchmarks.module.ts} | 11 +- .../benchmarks/benchmarks.service.ts | 163 +++++++++++++++++ apps/api/src/app/info/info.module.ts | 2 +- apps/api/src/app/info/info.service.ts | 2 +- .../services/benchmark/benchmark.module.ts | 24 +++ .../benchmark/benchmark.service.spec.ts | 12 +- .../benchmark/benchmark.service.ts | 165 +----------------- .../interfaces/benchmark-value.interface.ts | 0 .../twitter-bot/twitter-bot.module.ts | 2 +- .../twitter-bot/twitter-bot.service.ts | 2 +- apps/client/src/app/services/data.service.ts | 8 +- 15 files changed, 218 insertions(+), 191 deletions(-) rename apps/api/src/app/{benchmark/benchmark.controller.ts => endpoints/benchmarks/benchmarks.controller.ts} (93%) rename apps/api/src/app/{benchmark/benchmark.module.ts => endpoints/benchmarks/benchmarks.module.ts} (90%) create mode 100644 apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts create mode 100644 apps/api/src/services/benchmark/benchmark.module.ts rename apps/api/src/{app => services}/benchmark/benchmark.service.spec.ts (74%) rename apps/api/src/{app => services}/benchmark/benchmark.service.ts (65%) rename apps/api/src/{app => services}/benchmark/interfaces/benchmark-value.interface.ts (100%) diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts index 81c58ff0..d94cdd96 100644 --- a/apps/api/src/app/admin/admin.module.ts +++ b/apps/api/src/app/admin/admin.module.ts @@ -1,8 +1,8 @@ -import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index fb6e90f5..14210972 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -1,7 +1,7 @@ -import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { environment } from '@ghostfolio/api/environments/environment'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 3e68bf42..2a515bf4 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -29,10 +29,10 @@ import { AppController } from './app.controller'; import { AssetModule } from './asset/asset.module'; import { AuthDeviceModule } from './auth-device/auth-device.module'; import { AuthModule } from './auth/auth.module'; -import { BenchmarkModule } from './benchmark/benchmark.module'; import { CacheModule } from './cache/cache.module'; import { AiModule } from './endpoints/ai/ai.module'; import { ApiKeysModule } from './endpoints/api-keys/api-keys.module'; +import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module'; import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; import { MarketDataModule } from './endpoints/market-data/market-data.module'; import { PublicModule } from './endpoints/public/public.module'; @@ -63,7 +63,7 @@ import { UserModule } from './user/user.module'; AssetModule, AuthDeviceModule, AuthModule, - BenchmarkModule, + BenchmarksModule, BullModule.forRoot({ redis: { db: parseInt(process.env.REDIS_DB ?? '0', 10), diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts similarity index 93% rename from apps/api/src/app/benchmark/benchmark.controller.ts rename to apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts index d1908101..69383a30 100644 --- a/apps/api/src/app/benchmark/benchmark.controller.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts @@ -3,6 +3,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard' import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import type { @@ -32,13 +33,14 @@ import { AuthGuard } from '@nestjs/passport'; import { DataSource } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; -import { BenchmarkService } from './benchmark.service'; +import { BenchmarksService } from './benchmarks.service'; -@Controller('benchmark') -export class BenchmarkController { +@Controller('benchmarks') +export class BenchmarksController { public constructor( private readonly apiService: ApiService, private readonly benchmarkService: BenchmarkService, + private readonly benchmarksService: BenchmarksService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -139,7 +141,7 @@ export class BenchmarkController { const withExcludedAccounts = withExcludedAccountsParam === 'true'; - return this.benchmarkService.getMarketDataForUser({ + return this.benchmarksService.getMarketDataForUser({ dataSource, dateRange, endDate, diff --git a/apps/api/src/app/benchmark/benchmark.module.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts similarity index 90% rename from apps/api/src/app/benchmark/benchmark.module.ts rename to apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts index 8c042872..a0f44362 100644 --- a/apps/api/src/app/benchmark/benchmark.module.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts @@ -11,6 +11,7 @@ import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; @@ -24,12 +25,11 @@ import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/sym import { Module } from '@nestjs/common'; -import { BenchmarkController } from './benchmark.controller'; -import { BenchmarkService } from './benchmark.service'; +import { BenchmarksController } from './benchmarks.controller'; +import { BenchmarksService } from './benchmarks.service'; @Module({ - controllers: [BenchmarkController], - exports: [BenchmarkService], + controllers: [BenchmarksController], imports: [ ApiModule, ConfigurationModule, @@ -52,6 +52,7 @@ import { BenchmarkService } from './benchmark.service'; AccountBalanceService, AccountService, BenchmarkService, + BenchmarksService, CurrentRateService, MarketDataService, PortfolioCalculatorFactory, @@ -59,4 +60,4 @@ import { BenchmarkService } from './benchmark.service'; RulesService ] }) -export class BenchmarkModule {} +export class BenchmarksModule {} diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts new file mode 100644 index 00000000..237f0d15 --- /dev/null +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts @@ -0,0 +1,163 @@ +import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; +import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; +import { + AssetProfileIdentifier, + BenchmarkMarketDataDetails, + Filter +} from '@ghostfolio/common/interfaces'; +import { DateRange, UserWithSettings } from '@ghostfolio/common/types'; + +import { Injectable, Logger } from '@nestjs/common'; +import { format, isSameDay } from 'date-fns'; +import { isNumber } from 'lodash'; + +@Injectable() +export class BenchmarksService { + public constructor( + private readonly benchmarkService: BenchmarkService, + private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly marketDataService: MarketDataService, + private readonly portfolioService: PortfolioService, + private readonly symbolService: SymbolService + ) {} + + public async getMarketDataForUser({ + dataSource, + dateRange, + endDate = new Date(), + filters, + impersonationId, + startDate, + symbol, + user, + withExcludedAccounts + }: { + dateRange: DateRange; + endDate?: Date; + filters?: Filter[]; + impersonationId: string; + startDate: Date; + user: UserWithSettings; + withExcludedAccounts?: boolean; + } & AssetProfileIdentifier): Promise { + const marketData: { date: string; value: number }[] = []; + const userCurrency = user.Settings.settings.baseCurrency; + const userId = user.id; + + const { chart } = await this.portfolioService.getPerformance({ + dateRange, + filters, + impersonationId, + userId, + withExcludedAccounts + }); + + const [currentSymbolItem, marketDataItems] = await Promise.all([ + this.symbolService.get({ + dataGatheringItem: { + dataSource, + symbol + } + }), + this.marketDataService.marketDataItems({ + orderBy: { + date: 'asc' + }, + where: { + dataSource, + symbol, + date: { + in: chart.map(({ date }) => { + return resetHours(parseDate(date)); + }) + } + } + }) + ]); + + const exchangeRates = + await this.exchangeRateDataService.getExchangeRatesByCurrency({ + startDate, + currencies: [currentSymbolItem.currency], + targetCurrency: userCurrency + }); + + const exchangeRateAtStartDate = + exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ + format(startDate, DATE_FORMAT) + ]; + + const marketPriceAtStartDate = marketDataItems?.find(({ date }) => { + return isSameDay(date, startDate); + })?.marketPrice; + + if (!marketPriceAtStartDate) { + Logger.error( + `No historical market data has been found for ${symbol} (${dataSource}) at ${format( + startDate, + DATE_FORMAT + )}`, + 'BenchmarkService' + ); + + return { marketData }; + } + + for (const marketDataItem of marketDataItems) { + const exchangeRate = + exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ + format(marketDataItem.date, DATE_FORMAT) + ]; + + const exchangeRateFactor = + isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate) + ? exchangeRate / exchangeRateAtStartDate + : 1; + + marketData.push({ + date: format(marketDataItem.date, DATE_FORMAT), + value: + marketPriceAtStartDate === 0 + ? 0 + : this.benchmarkService.calculateChangeInPercentage( + marketPriceAtStartDate, + marketDataItem.marketPrice * exchangeRateFactor + ) * 100 + }); + } + + const includesEndDate = isSameDay( + parseDate(marketData.at(-1).date), + endDate + ); + + if (currentSymbolItem?.marketPrice && !includesEndDate) { + const exchangeRate = + exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ + format(endDate, DATE_FORMAT) + ]; + + const exchangeRateFactor = + isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate) + ? exchangeRate / exchangeRateAtStartDate + : 1; + + marketData.push({ + date: format(endDate, DATE_FORMAT), + value: + this.benchmarkService.calculateChangeInPercentage( + marketPriceAtStartDate, + currentSymbolItem.marketPrice * exchangeRateFactor + ) * 100 + }); + } + + return { + marketData + }; + } +} diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts index 7903ac39..d7a5ed64 100644 --- a/apps/api/src/app/info/info.module.ts +++ b/apps/api/src/app/info/info.module.ts @@ -1,8 +1,8 @@ -import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'; import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 1af41520..78086023 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -1,7 +1,7 @@ -import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service'; import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; diff --git a/apps/api/src/services/benchmark/benchmark.module.ts b/apps/api/src/services/benchmark/benchmark.module.ts new file mode 100644 index 00000000..870ef244 --- /dev/null +++ b/apps/api/src/services/benchmark/benchmark.module.ts @@ -0,0 +1,24 @@ +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; +import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; +import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; + +import { Module } from '@nestjs/common'; + +import { BenchmarkService } from './benchmark.service'; + +@Module({ + exports: [BenchmarkService], + imports: [ + DataProviderModule, + MarketDataModule, + PrismaModule, + PropertyModule, + RedisCacheModule, + SymbolProfileModule + ], + providers: [BenchmarkService] +}) +export class BenchmarkModule {} diff --git a/apps/api/src/app/benchmark/benchmark.service.spec.ts b/apps/api/src/services/benchmark/benchmark.service.spec.ts similarity index 74% rename from apps/api/src/app/benchmark/benchmark.service.spec.ts rename to apps/api/src/services/benchmark/benchmark.service.spec.ts index 5371fcdc..833dbcdf 100644 --- a/apps/api/src/app/benchmark/benchmark.service.spec.ts +++ b/apps/api/src/services/benchmark/benchmark.service.spec.ts @@ -4,17 +4,7 @@ describe('BenchmarkService', () => { let benchmarkService: BenchmarkService; beforeAll(async () => { - benchmarkService = new BenchmarkService( - null, - null, - null, - null, - null, - null, - null, - null, - null - ); + benchmarkService = new BenchmarkService(null, null, null, null, null, null); }); it('calculateChangeInPercentage', async () => { diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts similarity index 65% rename from apps/api/src/app/benchmark/benchmark.service.ts rename to apps/api/src/services/benchmark/benchmark.service.ts index 3efbc74f..57105da7 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/services/benchmark/benchmark.service.ts @@ -1,8 +1,5 @@ -import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; -import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; @@ -11,31 +8,20 @@ import { CACHE_TTL_INFINITE, PROPERTY_BENCHMARKS } from '@ghostfolio/common/config'; -import { - DATE_FORMAT, - calculateBenchmarkTrend, - parseDate, - resetHours -} from '@ghostfolio/common/helper'; +import { calculateBenchmarkTrend } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, Benchmark, - BenchmarkMarketDataDetails, BenchmarkProperty, - BenchmarkResponse, - Filter + BenchmarkResponse } from '@ghostfolio/common/interfaces'; -import { - BenchmarkTrend, - DateRange, - UserWithSettings -} from '@ghostfolio/common/types'; +import { BenchmarkTrend } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; -import { addHours, format, isAfter, isSameDay, subDays } from 'date-fns'; -import { isNumber, uniqBy } from 'lodash'; +import { addHours, isAfter, subDays } from 'date-fns'; +import { uniqBy } from 'lodash'; import ms from 'ms'; import { BenchmarkValue } from './interfaces/benchmark-value.interface'; @@ -46,14 +32,11 @@ export class BenchmarkService { public constructor( private readonly dataProviderService: DataProviderService, - private readonly exchangeRateDataService: ExchangeRateDataService, private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, - private readonly portfolioService: PortfolioService, private readonly propertyService: PropertyService, private readonly redisCacheService: RedisCacheService, - private readonly symbolProfileService: SymbolProfileService, - private readonly symbolService: SymbolService + private readonly symbolProfileService: SymbolProfileService ) {} public calculateChangeInPercentage(baseValue: number, currentValue: number) { @@ -153,142 +136,6 @@ export class BenchmarkService { .sort((a, b) => a.name.localeCompare(b.name)); } - public async getMarketDataForUser({ - dataSource, - dateRange, - endDate = new Date(), - filters, - impersonationId, - startDate, - symbol, - user, - withExcludedAccounts - }: { - dateRange: DateRange; - endDate?: Date; - filters?: Filter[]; - impersonationId: string; - startDate: Date; - user: UserWithSettings; - withExcludedAccounts?: boolean; - } & AssetProfileIdentifier): Promise { - const marketData: { date: string; value: number }[] = []; - const userCurrency = user.Settings.settings.baseCurrency; - const userId = user.id; - - const { chart } = await this.portfolioService.getPerformance({ - dateRange, - filters, - impersonationId, - userId, - withExcludedAccounts - }); - - const [currentSymbolItem, marketDataItems] = await Promise.all([ - this.symbolService.get({ - dataGatheringItem: { - dataSource, - symbol - } - }), - this.marketDataService.marketDataItems({ - orderBy: { - date: 'asc' - }, - where: { - dataSource, - symbol, - date: { - in: chart.map(({ date }) => { - return resetHours(parseDate(date)); - }) - } - } - }) - ]); - - const exchangeRates = - await this.exchangeRateDataService.getExchangeRatesByCurrency({ - startDate, - currencies: [currentSymbolItem.currency], - targetCurrency: userCurrency - }); - - const exchangeRateAtStartDate = - exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ - format(startDate, DATE_FORMAT) - ]; - - const marketPriceAtStartDate = marketDataItems?.find(({ date }) => { - return isSameDay(date, startDate); - })?.marketPrice; - - if (!marketPriceAtStartDate) { - Logger.error( - `No historical market data has been found for ${symbol} (${dataSource}) at ${format( - startDate, - DATE_FORMAT - )}`, - 'BenchmarkService' - ); - - return { marketData }; - } - - for (const marketDataItem of marketDataItems) { - const exchangeRate = - exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ - format(marketDataItem.date, DATE_FORMAT) - ]; - - const exchangeRateFactor = - isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate) - ? exchangeRate / exchangeRateAtStartDate - : 1; - - marketData.push({ - date: format(marketDataItem.date, DATE_FORMAT), - value: - marketPriceAtStartDate === 0 - ? 0 - : this.calculateChangeInPercentage( - marketPriceAtStartDate, - marketDataItem.marketPrice * exchangeRateFactor - ) * 100 - }); - } - - const includesEndDate = isSameDay( - parseDate(marketData.at(-1).date), - endDate - ); - - if (currentSymbolItem?.marketPrice && !includesEndDate) { - const exchangeRate = - exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ - format(endDate, DATE_FORMAT) - ]; - - const exchangeRateFactor = - isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate) - ? exchangeRate / exchangeRateAtStartDate - : 1; - - marketData.push({ - date: format(endDate, DATE_FORMAT), - value: - this.calculateChangeInPercentage( - marketPriceAtStartDate, - currentSymbolItem.marketPrice * exchangeRateFactor - ) * 100 - }); - } - - return { - marketData - }; - } - public async addBenchmark({ dataSource, symbol diff --git a/apps/api/src/app/benchmark/interfaces/benchmark-value.interface.ts b/apps/api/src/services/benchmark/interfaces/benchmark-value.interface.ts similarity index 100% rename from apps/api/src/app/benchmark/interfaces/benchmark-value.interface.ts rename to apps/api/src/services/benchmark/interfaces/benchmark-value.interface.ts diff --git a/apps/api/src/services/twitter-bot/twitter-bot.module.ts b/apps/api/src/services/twitter-bot/twitter-bot.module.ts index 4a2b1589..80d53169 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.module.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.module.ts @@ -1,5 +1,5 @@ -import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service'; diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts index a32882ae..a17585c5 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.service.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -1,5 +1,5 @@ -import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ghostfolioFearAndGreedIndexDataSource, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 6782644e..0bc4ebcc 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -303,7 +303,7 @@ export class DataService { } public deleteBenchmark({ dataSource, symbol }: AssetProfileIdentifier) { - return this.http.delete(`/api/v1/benchmark/${dataSource}/${symbol}`); + return this.http.delete(`/api/v1/benchmarks/${dataSource}/${symbol}`); } public deleteOwnUser(aData: DeleteOwnUserDto) { @@ -358,7 +358,7 @@ export class DataService { } return this.http.get( - `/api/v1/benchmark/${dataSource}/${symbol}/${format( + `/api/v1/benchmarks/${dataSource}/${symbol}/${format( startDate, DATE_FORMAT )}`, @@ -367,7 +367,7 @@ export class DataService { } public fetchBenchmarks() { - return this.http.get('/api/v1/benchmark'); + return this.http.get('/api/v1/benchmarks'); } public fetchExport({ @@ -704,7 +704,7 @@ export class DataService { } public postBenchmark(benchmark: AssetProfileIdentifier) { - return this.http.post('/api/v1/benchmark', benchmark); + return this.http.post('/api/v1/benchmarks', benchmark); } public postMarketData({ From 57673046e7d14c5fbaf9ec8b31aea90719a38972 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:52:25 +0100 Subject: [PATCH 7/8] Release 2.139.1 (#4326) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5be634..ac0f572d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 2.139.0 - 2025-02-15 +## 2.139.1 - 2025-02-15 ### Added diff --git a/package-lock.json b/package-lock.json index 430ab9fb..21cc622d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.139.0", + "version": "2.139.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.139.0", + "version": "2.139.1", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 587e88ca..a9717808 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.139.0", + "version": "2.139.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 02a4e270836ffb54d0f9aa0a3751e119e1dbec82 Mon Sep 17 00:00:00 2001 From: Ken Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sun, 16 Feb 2025 14:47:44 +0700 Subject: [PATCH 8/8] Bugfix/missing assets in Storybook (#4324) * fix(sb): add staticDirs config * fix(sb): improve config type safety * Update changelog --- CHANGELOG.md | 6 ++++++ libs/ui/.storybook/main.js | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0f572d..9c26756a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Added missing assets in _Storybook_ setup + ## 2.139.1 - 2025-02-15 ### Added diff --git a/libs/ui/.storybook/main.js b/libs/ui/.storybook/main.js index 8c8323a0..5b50ae06 100644 --- a/libs/ui/.storybook/main.js +++ b/libs/ui/.storybook/main.js @@ -1,9 +1,16 @@ +/** @type {import('@storybook/angular').StorybookConfig} */ const config = { addons: ['@storybook/addon-essentials'], framework: { name: '@storybook/angular', options: {} }, + staticDirs: [ + { + from: '../../../apps/client/src/assets', + to: '/assets' + } + ], stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'] };