Browse Source

Improve scraper (#28)

pull/27/head
Thomas 4 years ago
committed by GitHub
parent
commit
069006145a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      CHANGELOG.md
  2. 94
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 28
      apps/api/src/app/user/user.service.ts
  4. 104
      apps/api/src/models/portfolio.ts
  5. 17
      apps/api/src/services/data-gathering.service.ts
  6. 39
      apps/api/src/services/data-provider.service.ts
  7. 49
      apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts
  8. 3
      apps/client/src/app/app-routing.module.ts
  9. 4
      apps/client/src/app/components/position/position.component.html
  10. 23
      apps/client/src/app/pages/home/home-page.component.ts
  11. 4
      libs/helper/src/lib/helper.ts

10
CHANGELOG.md

@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Reverted the restoring of the scroll position when opening a new page
### Fixed
- Fixed some issues in the generic scraper
## 0.87.0 - 19.04.2021 ## 0.87.0 - 19.04.2021
### Added ### Added

94
apps/api/src/app/portfolio/portfolio.service.ts

@ -48,53 +48,6 @@ export class PortfolioService {
private readonly userService: UserService private readonly userService: UserService
) {} ) {}
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
public async createPortfolio(aUserId: string): Promise<Portfolio> { public async createPortfolio(aUserId: string): Promise<Portfolio> {
let portfolio: Portfolio; let portfolio: Portfolio;
let stringifiedPortfolio = await this.redisCacheService.get( let stringifiedPortfolio = await this.redisCacheService.get(
@ -382,4 +335,51 @@ export class PortfolioService {
symbol: aSymbol symbol: aSymbol
}; };
} }
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
} }

28
apps/api/src/app/user/user.service.ts

@ -44,10 +44,6 @@ export class UserService {
currentPermissions.push(permissions.accessFearAndGreedIndex); currentPermissions.push(permissions.accessFearAndGreedIndex);
} }
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
currentPermissions.push(permissions.useSocialLogin);
}
return { return {
alias, alias,
id, id,
@ -162,18 +158,6 @@ export class UserService {
}); });
} }
private getRandomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const result = [];
for (let i = 0; i < length; i++) {
result.push(
characters.charAt(Math.floor(Math.random() * characters.length))
);
}
return result.join('');
}
public async updateUserSettings({ public async updateUserSettings({
currency, currency,
userId userId
@ -200,4 +184,16 @@ export class UserService {
return; return;
} }
private getRandomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const result = [];
for (let i = 0; i < length; i++) {
result.push(
characters.charAt(Math.floor(Math.random() * characters.length))
);
}
return result.join('');
}
} }

104
apps/api/src/models/portfolio.ts

@ -79,7 +79,9 @@ export class Portfolio implements PortfolioInterface {
investmentInOriginalCurrency: investmentInOriginalCurrency:
portfolioItemsYesterday?.positions[symbol] portfolioItemsYesterday?.positions[symbol]
?.investmentInOriginalCurrency, ?.investmentInOriginalCurrency,
marketPrice: currentData[symbol]?.marketPrice, marketPrice:
currentData[symbol]?.marketPrice ??
portfolioItemsYesterday.positions[symbol]?.marketPrice,
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity quantity: portfolioItemsYesterday?.positions[symbol]?.quantity
}; };
}); });
@ -158,53 +160,6 @@ export class Portfolio implements PortfolioInterface {
return this; return this;
} }
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
public get(aDate?: Date): PortfolioItem[] { public get(aDate?: Date): PortfolioItem[] {
if (aDate) { if (aDate) {
const filteredPortfolio = this.portfolioItems.find((item) => { const filteredPortfolio = this.portfolioItems.find((item) => {
@ -528,12 +483,6 @@ export class Portfolio implements PortfolioInterface {
return this.orders; return this.orders;
} }
private getOrdersByType(aFilter: string[]) {
return this.orders.filter((order) => {
return aFilter.includes(order.getType());
});
}
public getValue(aDate = getToday()) { public getValue(aDate = getToday()) {
const positions = this.getPositions(aDate); const positions = this.getPositions(aDate);
let value = 0; let value = 0;
@ -692,6 +641,53 @@ export class Portfolio implements PortfolioInterface {
this.updatePortfolioItems(); this.updatePortfolioItems();
} }
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
private updatePortfolioItems() { private updatePortfolioItems() {
// console.time('update-portfolio-items'); // console.time('update-portfolio-items');

17
apps/api/src/services/data-gathering.service.ts

@ -2,6 +2,7 @@ import {
benchmarks, benchmarks,
currencyPairs, currencyPairs,
getUtc, getUtc,
isGhostfolioScraperApiSymbol,
resetHours resetHours
} from '@ghostfolio/helper'; } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -235,12 +236,16 @@ export class DataGatheringService {
select: { symbol: true } select: { symbol: true }
}); });
const distinctOrdersWithDate = distinctOrders.map((distinctOrder) => { const distinctOrdersWithDate = distinctOrders
return { .filter((distinctOrder) => {
...distinctOrder, return !isGhostfolioScraperApiSymbol(distinctOrder.symbol);
date: startDate })
}; .map((distinctOrder) => {
}); return {
...distinctOrder,
date: startDate
};
});
const currencyPairsToGather = currencyPairs.map((symbol) => { const currencyPairsToGather = currencyPairs.map((symbol) => {
return { return {

39
apps/api/src/services/data-provider.service.ts

@ -1,7 +1,7 @@
import { import {
isCrypto, isCrypto,
isGhostfolioScraperApi, isGhostfolioScraperApiSymbol,
isRakutenRapidApi isRakutenRapidApiSymbol
} from '@ghostfolio/helper'; } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { MarketData } from '@prisma/client'; import { MarketData } from '@prisma/client';
@ -39,14 +39,33 @@ export class DataProviderService implements DataProviderInterface {
if (aSymbols.length === 1) { if (aSymbols.length === 1) {
const symbol = aSymbols[0]; const symbol = aSymbols[0];
if (isGhostfolioScraperApi(symbol)) { if (isGhostfolioScraperApiSymbol(symbol)) {
return this.ghostfolioScraperApiService.get(aSymbols); return this.ghostfolioScraperApiService.get(aSymbols);
} else if (isRakutenRapidApi(symbol)) { } else if (isRakutenRapidApiSymbol(symbol)) {
return this.rakutenRapidApiService.get(aSymbols); return this.rakutenRapidApiService.get(aSymbols);
} }
} }
return this.yahooFinanceService.get(aSymbols); const yahooFinanceSymbols = aSymbols.filter((symbol) => {
return !isGhostfolioScraperApiSymbol(symbol);
});
const response = await this.yahooFinanceService.get(yahooFinanceSymbols);
const ghostfolioScraperApiSymbols = aSymbols.filter((symbol) => {
return isGhostfolioScraperApiSymbol(symbol);
});
for (const symbol of ghostfolioScraperApiSymbols) {
if (symbol) {
const ghostfolioScraperApiResult = await this.ghostfolioScraperApiService.get(
[symbol]
);
response[symbol] = ghostfolioScraperApiResult[symbol];
}
}
return response;
} }
public async getHistorical( public async getHistorical(
@ -107,8 +126,12 @@ export class DataProviderService implements DataProviderInterface {
): Promise<{ ): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
const filteredSymbols = aSymbols.filter((symbol) => {
return !isGhostfolioScraperApiSymbol(symbol);
});
const dataOfYahoo = await this.yahooFinanceService.getHistorical( const dataOfYahoo = await this.yahooFinanceService.getHistorical(
aSymbols, filteredSymbols,
undefined, undefined,
from, from,
to to
@ -135,7 +158,7 @@ export class DataProviderService implements DataProviderInterface {
...dataOfAlphaVantage[symbol] ...dataOfAlphaVantage[symbol]
} }
}; };
} else if (isGhostfolioScraperApi(symbol)) { } else if (isGhostfolioScraperApiSymbol(symbol)) {
const dataOfGhostfolioScraperApi = await this.ghostfolioScraperApiService.getHistorical( const dataOfGhostfolioScraperApi = await this.ghostfolioScraperApiService.getHistorical(
[symbol], [symbol],
undefined, undefined,
@ -145,7 +168,7 @@ export class DataProviderService implements DataProviderInterface {
return dataOfGhostfolioScraperApi; return dataOfGhostfolioScraperApi;
} else if ( } else if (
isRakutenRapidApi(symbol) && isRakutenRapidApiSymbol(symbol) &&
this.configurationService.get('RAKUTEN_RAPID_API_KEY') this.configurationService.get('RAKUTEN_RAPID_API_KEY')
) { ) {
const dataOfRakutenRapidApi = await this.rakutenRapidApiService.getHistorical( const dataOfRakutenRapidApi = await this.rakutenRapidApiService.getHistorical(

49
apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts

@ -11,7 +11,6 @@ import {
IDataProviderResponse IDataProviderResponse
} from '../../interfaces/interfaces'; } from '../../interfaces/interfaces';
import { PrismaService } from '../../prisma.service'; import { PrismaService } from '../../prisma.service';
import { Currency } from '.prisma/client';
@Injectable() @Injectable()
export class GhostfolioScraperApiService implements DataProviderInterface { export class GhostfolioScraperApiService implements DataProviderInterface {
@ -26,6 +25,9 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
try { try {
const symbol = aSymbols[0]; const symbol = aSymbols[0];
const scraperConfig = await this.getScraperConfig(symbol);
const { marketPrice } = await this.prisma.marketData.findFirst({ const { marketPrice } = await this.prisma.marketData.findFirst({
orderBy: { orderBy: {
date: 'desc' date: 'desc'
@ -38,9 +40,9 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return { return {
[symbol]: { [symbol]: {
marketPrice, marketPrice,
currency: Currency.CHF, currency: scraperConfig?.currency,
isMarketOpen: true, isMarketOpen: false,
name: symbol name: scraperConfig?.name
} }
}; };
} catch (error) { } catch (error) {
@ -65,25 +67,17 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
try { try {
const symbol = aSymbols[0]; const symbol = aSymbols[0];
const { const scraperConfig = await this.getScraperConfig(symbol);
value: scraperConfigString
} = await this.prisma.property.findFirst({
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
const scraperConfig = JSON.parse(scraperConfigString).find((item) => {
return item.symbol === symbol;
});
const get = bent(scraperConfig.url, 'GET', 'string', 200, {}); const get = bent(scraperConfig?.url, 'GET', 'string', 200, {});
const html = await get(); const html = await get();
const $ = cheerio.load(html); const $ = cheerio.load(html);
const string = $(scraperConfig.selector).text().replace('CHF', '').trim(); const string = $(scraperConfig?.selector)
.text()
.replace('CHF', '')
.trim();
const value = parseFloat(string); const value = parseFloat(string);
@ -100,4 +94,23 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return {}; return {};
} }
private async getScraperConfig(aSymbol: string) {
try {
const {
value: scraperConfigString
} = await this.prisma.property.findFirst({
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
return JSON.parse(scraperConfigString).find((item) => {
return item.symbol === aSymbol;
});
} catch {}
return {};
}
} }

3
apps/client/src/app/app-routing.module.ts

@ -81,8 +81,7 @@ const routes: Routes = [
{ {
preloadingStrategy: ModulePreloadService, preloadingStrategy: ModulePreloadService,
// enableTracing: true // <-- debugging purposes only // enableTracing: true // <-- debugging purposes only
relativeLinkResolution: 'legacy', relativeLinkResolution: 'legacy'
scrollPositionRestoration: 'enabled'
} }
) )
], ],

4
apps/client/src/app/components/position/position.component.html

@ -40,7 +40,9 @@
class="ml-1" class="ml-1"
[url]="position?.url" [url]="position?.url"
></gf-symbol-icon> ></gf-symbol-icon>
<span class="ml-2 text-muted">({{ position?.exchange }})</span> <span *ngIf="position?.exchange" class="ml-2 text-muted"
>({{ position.exchange }})</span
>
</div> </div>
<div class="d-flex mt-1"> <div class="d-flex mt-1">
<gf-value <gf-value

23
apps/client/src/app/pages/home/home-page.component.ts

@ -85,6 +85,18 @@ export class HomePageComponent implements OnDestroy, OnInit {
user.permissions, user.permissions,
permissions.accessFearAndGreedIndex permissions.accessFearAndGreedIndex
); );
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem('GF.FEAR_AND_GREED_INDEX')
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.cd.markForCheck();
});
}
this.hasPermissionToReadForeignPortfolio = hasPermission( this.hasPermissionToReadForeignPortfolio = hasPermission(
user.permissions, user.permissions,
permissions.readForeignPortfolio permissions.readForeignPortfolio
@ -180,17 +192,6 @@ export class HomePageComponent implements OnDestroy, OnInit {
this.cd.markForCheck(); this.cd.markForCheck();
}); });
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem('GF.FEAR_AND_GREED_INDEX')
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.cd.markForCheck();
});
}
this.cd.markForCheck(); this.cd.markForCheck();
} }

4
libs/helper/src/lib/helper.ts

@ -66,11 +66,11 @@ export function isCurrency(aSymbol = '') {
); );
} }
export function isGhostfolioScraperApi(aSymbol = '') { export function isGhostfolioScraperApiSymbol(aSymbol = '') {
return aSymbol.startsWith('[GF]'); return aSymbol.startsWith('[GF]');
} }
export function isRakutenRapidApi(aSymbol = '') { export function isRakutenRapidApiSymbol(aSymbol = '') {
return aSymbol === 'GF.FEAR_AND_GREED_INDEX'; return aSymbol === 'GF.FEAR_AND_GREED_INDEX';
} }

Loading…
Cancel
Save