Browse Source

Feature/extend watchlist endpoint by name, performances and market condition (#4634)

* Extend watchlist endpoint by name, performances and market condition

* Update changelog
pull/4632/head^2
Kenrick Tandrian 1 day ago
committed by GitHub
parent
commit
770b322137
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 4
      apps/api/src/app/endpoints/watchlist/watchlist.module.ts
  3. 54
      apps/api/src/app/endpoints/watchlist/watchlist.service.ts
  4. 24
      apps/api/src/services/benchmark/benchmark.service.ts
  5. 21
      apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
  6. 2
      apps/client/src/app/components/home-watchlist/home-watchlist.html
  7. 11
      libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Extended the watchlist by the date of the last all time high, the current change to the all time high and the current market condition (experimental)
### Changed
- Improved the language localization for Français (`fr`)

4
apps/api/src/app/endpoints/watchlist/watchlist.module.ts

@ -1,6 +1,8 @@
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 { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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 { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -13,8 +15,10 @@ import { WatchlistService } from './watchlist.service';
@Module({
controllers: [WatchlistController],
imports: [
BenchmarkModule,
DataGatheringModule,
DataProviderModule,
MarketDataModule,
PrismaModule,
SymbolProfileModule,
TransformDataSourceInRequestModule,

54
apps/api/src/app/endpoints/watchlist/watchlist.service.ts

@ -1,8 +1,10 @@
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { WatchlistResponse } from '@ghostfolio/common/interfaces';
import { BadRequestException, Injectable } from '@nestjs/common';
import { DataSource, Prisma } from '@prisma/client';
@ -10,8 +12,10 @@ import { DataSource, Prisma } from '@prisma/client';
@Injectable()
export class WatchlistService {
public constructor(
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -87,7 +91,7 @@ export class WatchlistService {
public async getWatchlistItems(
userId: string
): Promise<AssetProfileIdentifier[]> {
): Promise<WatchlistResponse['watchlist']> {
const user = await this.prismaService.user.findUnique({
select: {
watchlist: {
@ -97,6 +101,50 @@ export class WatchlistService {
where: { id: userId }
});
return user.watchlist ?? [];
const [assetProfiles, quotes] = await Promise.all([
this.symbolProfileService.getSymbolProfiles(user.watchlist),
this.dataProviderService.getQuotes({
items: user.watchlist.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
})
})
]);
const watchlist = await Promise.all(
user.watchlist.map(async ({ dataSource, symbol }) => {
const assetProfile = assetProfiles.find((profile) => {
return profile.dataSource === dataSource && profile.symbol === symbol;
});
const allTimeHigh = await this.marketDataService.getMax({
dataSource,
symbol
});
const performancePercent =
this.benchmarkService.calculateChangeInPercentage(
allTimeHigh?.marketPrice,
quotes[symbol]?.marketPrice
);
return {
dataSource,
symbol,
marketCondition:
this.benchmarkService.getMarketCondition(performancePercent),
name: assetProfile?.name,
performances: {
allTimeHigh: {
performancePercent,
date: allTimeHigh?.date
}
}
};
})
);
return watchlist.sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
}

24
apps/api/src/services/benchmark/benchmark.service.ts

@ -212,6 +212,18 @@ export class BenchmarkService {
};
}
public getMarketCondition(
aPerformanceInPercent: number
): Benchmark['marketCondition'] {
if (aPerformanceInPercent >= 0) {
return 'ALL_TIME_HIGH';
} else if (aPerformanceInPercent <= -0.2) {
return 'BEAR_MARKET';
} else {
return 'NEUTRAL_MARKET';
}
}
private async calculateAndCacheBenchmarks({
enableSharing = false
}): Promise<BenchmarkResponse['benchmarks']> {
@ -302,16 +314,4 @@ export class BenchmarkService {
return benchmarks;
}
private getMarketCondition(
aPerformanceInPercent: number
): Benchmark['marketCondition'] {
if (aPerformanceInPercent >= 0) {
return 'ALL_TIME_HIGH';
} else if (aPerformanceInPercent <= -0.2) {
return 'BEAR_MARKET';
} else {
return 'NEUTRAL_MARKET';
}
}
}

21
apps/client/src/app/components/home-watchlist/home-watchlist.component.ts

@ -6,6 +6,7 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { BenchmarkTrend } from '@ghostfolio/common/types';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -118,15 +119,17 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
.fetchWatchlist()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ watchlist }) => {
this.watchlist = watchlist.map(({ dataSource, symbol }) => ({
dataSource,
symbol,
marketCondition: null,
name: symbol,
performances: null,
trend50d: 'UNKNOWN',
trend200d: 'UNKNOWN'
}));
this.watchlist = watchlist.map(
({ dataSource, marketCondition, name, performances, symbol }) => ({
dataSource,
marketCondition,
name,
performances,
symbol,
trend50d: 'UNKNOWN' as BenchmarkTrend,
trend200d: 'UNKNOWN' as BenchmarkTrend
})
);
this.changeDetectorRef.markForCheck();
});

2
apps/client/src/app/components/home-watchlist/home-watchlist.html

@ -7,7 +7,7 @@
}
</span>
</h1>
<div class="mb-3 row">
<div class="row">
<div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark
[benchmarks]="watchlist"

11
libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts

@ -1,5 +1,12 @@
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import {
AssetProfileIdentifier,
Benchmark
} from '@ghostfolio/common/interfaces';
export interface WatchlistResponse {
watchlist: AssetProfileIdentifier[];
watchlist: (AssetProfileIdentifier & {
marketCondition: Benchmark['marketCondition'];
name: string;
performances: Benchmark['performances'];
})[];
}

Loading…
Cancel
Save