Browse Source

Feature/add markets endpoint for Fear & Greed index to Ghostfolio data provider (#7323)

* Add markets endpoint

* Update changelog
pull/7326/head
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
69254032a3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 39
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  3. 2
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts
  4. 33
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  5. 43
      apps/api/src/app/endpoints/market-data/market-data.controller.ts
  6. 8
      apps/api/src/app/endpoints/market-data/market-data.module.ts
  7. 46
      apps/api/src/app/symbol/symbol.service.ts
  8. 22
      apps/client/src/app/pages/api/api-page.component.ts
  9. 52
      apps/client/src/app/pages/api/api-page.html

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Added the markets endpoint for the _Fear & Greed Index_ (market mood) to the `GHOSTFOLIO` data provider
### Changed
- Hardened the validation of the countries in the asset profile endpoints

39
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -8,6 +8,7 @@ import {
DividendsResponse,
HistoricalResponse,
LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
@ -19,6 +20,7 @@ import {
HttpException,
Inject,
Param,
ParseIntPipe,
Query,
UseGuards,
Version
@ -203,6 +205,43 @@ export class GhostfolioController {
}
}
@Get('markets')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard)
public async getMarketDataOfMarkets(
@Query('includeHistoricalData', new ParseIntPipe({ optional: true }))
includeHistoricalData = 0
): Promise<MarketDataOfMarketsResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const marketDataOfMarkets =
await this.ghostfolioService.getMarketDataOfMarkets({
includeHistoricalData
});
await this.ghostfolioService.incrementDailyRequests({
userId: this.request.user.id
});
return marketDataOfMarkets;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get('quotes')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard)

2
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts

@ -1,4 +1,5 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
@ -33,6 +34,7 @@ import { GhostfolioService } from './ghostfolio.service';
PrismaModule,
PropertyModule,
RedisCacheModule,
SymbolModule,
SymbolProfileModule
],
providers: [

33
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -1,3 +1,4 @@
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 { GhostfolioService as GhostfolioDataProviderService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service';
@ -25,6 +26,7 @@ import {
HistoricalResponse,
LookupItem,
LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
@ -32,6 +34,7 @@ import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
import { isEmpty } from 'lodash';
@Injectable()
export class GhostfolioService {
@ -42,7 +45,8 @@ export class GhostfolioService {
private readonly dataProviderService: DataProviderService,
private readonly fetchService: FetchService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService
private readonly propertyService: PropertyService,
private readonly symbolService: SymbolService
) {}
public async getAssetProfile({ symbol }: GetAssetProfileParams) {
@ -198,6 +202,33 @@ export class GhostfolioService {
}
}
public async getMarketDataOfMarkets({
includeHistoricalData
}: {
includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> {
try {
const marketDataOfMarkets =
await this.symbolService.getMarketDataOfMarkets({
includeHistoricalData
});
for (const symbolItem of Object.values(
marketDataOfMarkets.fearAndGreedIndex
)) {
if (!isEmpty(symbolItem)) {
symbolItem.dataSource = DataSource.GHOSTFOLIO;
}
}
return marketDataOfMarkets;
} catch (error) {
this.logger.error(error);
throw error;
}
}
public async getMaxDailyRequests() {
return parseInt(
(await this.propertyService.getByKey<string>(

43
apps/api/src/app/endpoints/market-data/market-data.controller.ts

@ -1,14 +1,8 @@
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks
} from '@ghostfolio/common/config';
import { UpdateBulkMarketDataDto } from '@ghostfolio/common/dtos';
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces';
@ -36,7 +30,6 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes';
@Controller('market-data')
export class MarketDataController {
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService,
@ -50,39 +43,9 @@ export class MarketDataController {
@Query('includeHistoricalData', new ParseIntPipe({ optional: true }))
includeHistoricalData = 0
): Promise<MarketDataOfMarketsResponse> {
const [
marketDataFearAndGreedIndexCryptocurrencies,
marketDataFearAndGreedIndexStocks
] = await Promise.all([
this.symbolService.get({
includeHistoricalData,
dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
symbol: ghostfolioFearAndGreedIndexSymbolCryptocurrencies
},
useIntradayData: true
}),
this.symbolService.get({
includeHistoricalData,
dataGatheringItem: {
dataSource:
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(),
symbol: ghostfolioFearAndGreedIndexSymbolStocks
},
useIntradayData: true
})
]);
return {
fearAndGreedIndex: {
CRYPTOCURRENCIES: {
...marketDataFearAndGreedIndexCryptocurrencies
},
STOCKS: {
...marketDataFearAndGreedIndexStocks
}
}
};
return this.symbolService.getMarketDataOfMarkets({
includeHistoricalData
});
}
@Post(':dataSource/:symbol')

8
apps/api/src/app/endpoints/market-data/market-data.module.ts

@ -1,5 +1,4 @@
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -9,11 +8,6 @@ import { MarketDataController } from './market-data.controller';
@Module({
controllers: [MarketDataController],
imports: [
DataProviderModule,
MarketDataServiceModule,
SymbolModule,
SymbolProfileModule
]
imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule]
})
export class MarketDataModule {}

46
apps/api/src/app/symbol/symbol.service.ts

@ -1,6 +1,11 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import {
ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks
} from '@ghostfolio/common/config';
import {
DATE_FORMAT,
getAssetProfileIdentifier
@ -9,6 +14,7 @@ import {
DataProviderHistoricalResponse,
HistoricalDataItem,
LookupResponse,
MarketDataOfMarketsResponse,
SymbolItem
} from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
@ -122,6 +128,46 @@ export class SymbolService {
};
}
public async getMarketDataOfMarkets({
includeHistoricalData
}: {
includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> {
const [
marketDataFearAndGreedIndexCryptocurrencies,
marketDataFearAndGreedIndexStocks
] = await Promise.all([
this.get({
includeHistoricalData,
dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
symbol: ghostfolioFearAndGreedIndexSymbolCryptocurrencies
},
useIntradayData: true
}),
this.get({
includeHistoricalData,
dataGatheringItem: {
dataSource:
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(),
symbol: ghostfolioFearAndGreedIndexSymbolStocks
},
useIntradayData: true
})
]);
return {
fearAndGreedIndex: {
CRYPTOCURRENCIES: {
...marketDataFearAndGreedIndexCryptocurrencies
},
STOCKS: {
...marketDataFearAndGreedIndexStocks
}
}
};
}
public async lookup({
includeIndices = false,
query,

22
apps/client/src/app/pages/api/api-page.component.ts

@ -1,3 +1,4 @@
import { GfFearAndGreedIndexComponent } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.component';
import {
HEADER_KEY_SKIP_INTERCEPTOR,
HEADER_KEY_TOKEN
@ -10,6 +11,7 @@ import {
DividendsResponse,
HistoricalResponse,
LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
@ -32,7 +34,12 @@ import { FetchFailure, FetchResult } from './interfaces/interfaces';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatCardModule, NgxSkeletonLoaderModule],
imports: [
CommonModule,
GfFearAndGreedIndexComponent,
MatCardModule,
NgxSkeletonLoaderModule
],
selector: 'gf-api-page',
styleUrls: ['./api-page.scss'],
templateUrl: './api-page.html'
@ -48,6 +55,9 @@ export class GfApiPageComponent implements OnInit {
>;
protected isinLookupItems$: Observable<FetchResult<LookupResponse['items']>>;
protected lookupItems$: Observable<FetchResult<LookupResponse['items']>>;
protected marketDataOfMarkets$: Observable<
FetchResult<MarketDataOfMarketsResponse>
>;
protected quotes$: Observable<FetchResult<QuotesResponse['quotes']>>;
protected status$: Observable<
FetchResult<DataProviderGhostfolioStatusResponse>
@ -68,6 +78,7 @@ export class GfApiPageComponent implements OnInit {
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' });
this.isinLookupItems$ = this.fetchLookupItems({ query: 'US0378331005' });
this.lookupItems$ = this.fetchLookupItems({ query: 'apple' });
this.marketDataOfMarkets$ = this.fetchMarketDataOfMarkets();
this.quotes$ = this.fetchQuotes({ symbols: ['AAPL', 'VOO'] });
this.status$ = this.fetchStatus();
}
@ -172,6 +183,15 @@ export class GfApiPageComponent implements OnInit {
);
}
private fetchMarketDataOfMarkets() {
return this.http
.get<MarketDataOfMarketsResponse>(
'/api/v1/data-providers/ghostfolio/markets',
{ headers: this.getHeaders() }
)
.pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef));
}
private fetchQuotes({ symbols }: { symbols: string[] }) {
const params = new HttpParams().set('symbols', symbols.join(','));

52
apps/client/src/app/pages/api/api-page.html

@ -168,6 +168,58 @@
}
</mat-card-content>
</mat-card>
<div class="row">
@let marketDataOfMarkets = marketDataOfMarkets$ | async;
<div class="col-xs-12 col-md-6">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title class="mb-0">Markets</mat-card-title>
<mat-card-subtitle>Stocks</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="pt-3">
@if (isFetchFailure(marketDataOfMarkets)) {
🔴 {{ marketDataOfMarkets.fetchError }}
} @else if (marketDataOfMarkets) {
<gf-fear-and-greed-index
[fearAndGreedIndex]="
marketDataOfMarkets.fearAndGreedIndex.STOCKS.marketPrice
"
/>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '2.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
</div>
<div class="col-xs-12 col-md-6">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title class="mb-0">Markets</mat-card-title>
<mat-card-subtitle>Cryptocurrencies</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="pt-3">
@if (isFetchFailure(marketDataOfMarkets)) {
🔴 {{ marketDataOfMarkets.fetchError }}
} @else if (marketDataOfMarkets) {
<gf-fear-and-greed-index
[fearAndGreedIndex]="
marketDataOfMarkets.fearAndGreedIndex.CRYPTOCURRENCIES
.marketPrice
"
/>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '2.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</div>
</div>

Loading…
Cancel
Save