diff --git a/.env b/.env.example similarity index 100% rename from .env rename to .env.example diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 6b85f639b..26ccb9b8d 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -41,7 +41,7 @@ jobs: uses: docker/build-push-action@v3 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm/v7,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.output.labels }} diff --git a/.gitignore b/.gitignore index 7440c445a..307d7e9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ # misc /.angular/cache +.env .env.prod /.sass-cache /connect.lock @@ -38,4 +39,4 @@ yarn-error.log # System Files .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b9868e0..e6473901c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added a blog post: _Ghostfolio meets Umbrel_ + ### Changed - Removed the dependency `rimraf` +## 1.238.0 - 2023-02-25 + +### Added + +- Added `COINGECKO` as a new data source type +- Added support for data provider information to the position detail dialog +- Added the configuration to publish a `linux/arm/v7` docker image +- Added _Reddit_ to the _As seen in_ section on the landing page +- Added _Umbrel_ to the _As seen in_ section on the landing page + +### Changed + +- Renamed the example environment variable file from `.env` to `.env.example` +- Upgraded `zone.js` from version `0.11.8` to `0.12.0` + +### Fixed + +- Fixed `RangeError: Maximum call stack size exceeded` for values of type `Big` in the value redaction interceptor for the impersonation mode +- Reset the letter spacing in buttons + +### Todo + +- Ensure that you still have a `.env` file in your project + ## 1.237.0 - 2023-02-19 ### Added @@ -763,7 +791,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the alias to the `Access` database schema - Added support for translated time distances -- Added a _GitHub Action_ to create an `arm64` docker image +- Added a _GitHub Action_ to create an `linux/arm64` docker image ### Changed diff --git a/README.md b/README.md index 1c319db54..aebc7129f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater ## Self-hosting -We provide official container images hosted on [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio) for `linux/amd64` and `linux/arm64`. +We provide official container images hosted on [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio) for `linux/amd64`, `linux/arm/v7` and `linux/arm64`.
@@ -106,7 +106,8 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c - Basic knowledge of Docker - Installation of [Docker](https://www.docker.com/products/docker-desktop) -- Local copy of this Git repository (clone) +- Create a local copy of this Git repository (clone) +- Copy the file `.env.example` to `.env` and populate it with your data (`cp .env.example .env`) #### a. Run environment @@ -150,7 +151,8 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https:// - [Docker](https://www.docker.com/products/docker-desktop) - [Node.js](https://nodejs.org/en/download) (version 16) - [Yarn](https://yarnpkg.com/en/docs/install) -- A local copy of this Git repository (clone) +- Create a local copy of this Git repository (clone) +- Copy the file `.env.example` to `.env` and populate it with your data (`cp .env.example .env`) ### Setup diff --git a/apps/api/src/app/frontend.middleware.ts b/apps/api/src/app/frontend.middleware.ts index 8165af216..9376f5c43 100644 --- a/apps/api/src/app/frontend.middleware.ts +++ b/apps/api/src/app/frontend.middleware.ts @@ -90,6 +90,11 @@ export class FrontendMiddleware implements NestMiddleware { ) { featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png'; title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`; + } else if ( + request.path.startsWith('/de/blog/2023/02/ghostfolio-meets-umbrel') + ) { + featureGraphicPath = 'assets/images/blog/ghostfolio-meets-umbrel.png'; + title = `Ghostfolio meets Umbrel - ${title}`; } if ( diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 077f2227b..2d0cb7376 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -110,9 +110,6 @@ export class OrderService { dataSource, symbol: id }; - } else { - data.SymbolProfile.connectOrCreate.create.symbol = - data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase(); } await this.dataGatheringService.addJobToQueue( diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 004f642bf..cac110f05 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -1,4 +1,5 @@ import { parseDate, resetHours } from '@ghostfolio/common/helper'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns'; import { GetValueObject } from './interfaces/get-value-object.interface'; @@ -48,8 +49,11 @@ export const CurrentRateServiceMock = { getValues: ({ dataGatheringItems, dateQuery - }: GetValuesParams): Promise => { - const result: GetValueObject[] = []; + }: GetValuesParams): Promise<{ + dataProviderInfos: DataProviderInfo[]; + values: GetValueObject[]; + }> => { + const values: GetValueObject[] = []; if (dateQuery.lt) { for ( let date = resetHours(dateQuery.gte); @@ -57,7 +61,7 @@ export const CurrentRateServiceMock = { date = addDays(date, 1) ) { for (const dataGatheringItem of dataGatheringItems) { - result.push({ + values.push({ date, marketPriceInBaseCurrency: mockGetValue( dataGatheringItem.symbol, @@ -70,7 +74,7 @@ export const CurrentRateServiceMock = { } else { for (const date of dateQuery.in) { for (const dataGatheringItem of dataGatheringItems) { - result.push({ + values.push({ date, marketPriceInBaseCurrency: mockGetValue( dataGatheringItem.symbol, @@ -81,6 +85,6 @@ export const CurrentRateServiceMock = { } } } - return Promise.resolve(result); + return Promise.resolve({ values, dataProviderInfos: [] }); } }; diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts index 9528f980f..9171e5fa3 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -1,6 +1,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { DataSource, MarketData } from '@prisma/client'; import { CurrentRateService } from './current-rate.service'; @@ -103,17 +104,23 @@ describe('CurrentRateService', () => { }, userCurrency: 'CHF' }) - ).toMatchObject([ - { - date: undefined, - marketPriceInBaseCurrency: 1841.823902, - symbol: 'AMZN' - }, - { - date: undefined, - marketPriceInBaseCurrency: 1847.839966, - symbol: 'AMZN' - } - ]); + ).toMatchObject<{ + dataProviderInfos: DataProviderInfo[]; + values: GetValueObject[]; + }>({ + dataProviderInfos: [], + values: [ + { + date: undefined, + marketPriceInBaseCurrency: 1841.823902, + symbol: 'AMZN' + }, + { + date: undefined, + marketPriceInBaseCurrency: 1847.839966, + symbol: 'AMZN' + } + ] + }); }); }); diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index 86020ce2e..b3a432521 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { resetHours } from '@ghostfolio/common/helper'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { isBefore, isToday } from 'date-fns'; import { flatten } from 'lodash'; @@ -22,7 +23,11 @@ export class CurrentRateService { dataGatheringItems, dateQuery, userCurrency - }: GetValuesParams): Promise { + }: GetValuesParams): Promise<{ + dataProviderInfos: DataProviderInfo[]; + values: GetValueObject[]; + }> { + const dataProviderInfos: DataProviderInfo[] = []; const includeToday = (!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) && (!dateQuery.gte || isBefore(dateQuery.gte, new Date())) && @@ -38,6 +43,14 @@ export class CurrentRateService { .then((dataResultProvider) => { const result: GetValueObject[] = []; for (const dataGatheringItem of dataGatheringItems) { + if ( + dataResultProvider?.[dataGatheringItem.symbol]?.dataProviderInfo + ) { + dataProviderInfos.push( + dataResultProvider[dataGatheringItem.symbol].dataProviderInfo + ); + } + result.push({ date: today, marketPriceInBaseCurrency: @@ -81,7 +94,10 @@ export class CurrentRateService { }) ); - return flatten(await Promise.all(promises)); + return { + dataProviderInfos, + values: flatten(await Promise.all(promises)) + }; } private containsToday(dates: Date[]): boolean { diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index 48a0ac671..827aa25fe 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -1,4 +1,5 @@ import { + DataProviderInfo, EnhancedSymbolProfile, HistoricalDataItem } from '@ghostfolio/common/interfaces'; @@ -7,6 +8,7 @@ import { Tag } from '@prisma/client'; export interface PortfolioPositionDetail { averagePrice: number; + dataProviderInfo: DataProviderInfo; dividendInBaseCurrency: number; feeInBaseCurrency: number; firstBuyDate: string; diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index f99defd53..06c617166 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -1,7 +1,11 @@ import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; -import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; +import { + DataProviderInfo, + ResponseError, + TimelinePosition +} from '@ghostfolio/common/interfaces'; import { GroupBy } from '@ghostfolio/common/types'; import { Logger } from '@nestjs/common'; import { Type as TypeOfOrder } from '@prisma/client'; @@ -45,6 +49,7 @@ export class PortfolioCalculator { private currency: string; private currentRateService: CurrentRateService; + private dataProviderInfos: DataProviderInfo[]; private orders: PortfolioOrder[]; private transactionPoints: TransactionPoint[]; @@ -202,14 +207,17 @@ export class PortfolioCalculator { symbols[item.symbol] = true; } - const marketSymbols = await this.currentRateService.getValues({ - currencies, - dataGatheringItems, - dateQuery: { - in: dates - }, - userCurrency: this.currency - }); + const { dataProviderInfos, values: marketSymbols } = + await this.currentRateService.getValues({ + currencies, + dataGatheringItems, + dateQuery: { + in: dates + }, + userCurrency: this.currency + }); + + this.dataProviderInfos = dataProviderInfos; const marketSymbolMap: { [date: string]: { [symbol: string]: Big }; @@ -368,14 +376,17 @@ export class PortfolioCalculator { dates.push(resetHours(end)); - const marketSymbols = await this.currentRateService.getValues({ - currencies, - dataGatheringItems, - dateQuery: { - in: dates - }, - userCurrency: this.currency - }); + const { dataProviderInfos, values: marketSymbols } = + await this.currentRateService.getValues({ + currencies, + dataGatheringItems, + dateQuery: { + in: dates + }, + userCurrency: this.currency + }); + + this.dataProviderInfos = dataProviderInfos; const marketSymbolMap: { [date: string]: { [symbol: string]: Big }; @@ -463,6 +474,10 @@ export class PortfolioCalculator { }; } + public getDataProviderInfos() { + return this.dataProviderInfos; + } + public getInvestments(): { date: string; investment: Big }[] { if (this.transactionPoints.length === 0) { return []; @@ -748,7 +763,7 @@ export class PortfolioCalculator { let marketSymbols: GetValueObject[] = []; if (dataGatheringItems.length > 0) { try { - marketSymbols = await this.currentRateService.getValues({ + const { values } = await this.currentRateService.getValues({ currencies, dataGatheringItems, dateQuery: { @@ -757,6 +772,7 @@ export class PortfolioCalculator { }, userCurrency: this.currency }); + marketSymbols = values; } catch (error) { Logger.error( `Failed to fetch info for date ${startDate} with exception`, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 2564d1d49..1677ffc29 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -678,6 +678,7 @@ export class PortfolioService { return { tags, averagePrice: undefined, + dataProviderInfo: undefined, dividendInBaseCurrency: undefined, feeInBaseCurrency: undefined, firstBuyDate: undefined, @@ -849,6 +850,7 @@ export class PortfolioService { tags, transactionCount, averagePrice: averagePrice.toNumber(), + dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), feeInBaseCurrency: this.exchangeRateDataService.toCurrency( fee.toNumber(), @@ -911,6 +913,7 @@ export class PortfolioService { SymbolProfile, tags, averagePrice: 0, + dataProviderInfo: undefined, dividendInBaseCurrency: 0, feeInBaseCurrency: 0, firstBuyDate: undefined, diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index 7ee07b468..6db53b0a1 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -1,3 +1,4 @@ +import Big from 'big.js'; import { cloneDeep, isArray, isObject } from 'lodash'; export function hasNotDefinedValuesInObject(aObject: Object): boolean { @@ -59,7 +60,10 @@ export function redactAttributes({ return redactAttributes({ options, object: currentObject }); } ); - } else if (isObject(redactedObject[property])) { + } else if ( + isObject(redactedObject[property]) && + !(redactedObject[property] instanceof Big) + ) { // Recursively call the function on the nested object redactedObject[property] = redactAttributes({ options, diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts new file mode 100644 index 000000000..a96511246 --- /dev/null +++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts @@ -0,0 +1,200 @@ +import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; +import { Granularity } from '@ghostfolio/common/types'; +import { Injectable, Logger } from '@nestjs/common'; +import { + AssetClass, + AssetSubClass, + DataSource, + SymbolProfile +} from '@prisma/client'; +import bent from 'bent'; +import { format, fromUnixTime, getUnixTime } from 'date-fns'; + +@Injectable() +export class CoinGeckoService implements DataProviderInterface { + private baseCurrency: string; + private readonly URL = 'https://api.coingecko.com/api/v3'; + + public constructor( + private readonly configurationService: ConfigurationService + ) { + this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); + } + + public canHandle(symbol: string) { + return true; + } + + public async getAssetProfile( + aSymbol: string + ): Promise> { + const response: Partial = { + assetClass: AssetClass.CASH, + assetSubClass: AssetSubClass.CRYPTOCURRENCY, + currency: this.baseCurrency, + dataSource: this.getName(), + symbol: aSymbol + }; + + try { + const get = bent(`${this.URL}/coins/${aSymbol}`, 'GET', 'json', 200); + const { name } = await get(); + + response.name = name; + } catch (error) { + Logger.error(error, 'CoinGeckoService'); + } + + return response; + } + + public async getDividends({ + from, + granularity = 'day', + symbol, + to + }: { + from: Date; + granularity: Granularity; + symbol: string; + to: Date; + }) { + return {}; + } + + public async getHistorical( + aSymbol: string, + aGranularity: Granularity = 'day', + from: Date, + to: Date + ): Promise<{ + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + }> { + try { + const get = bent( + `${ + this.URL + }/coins/${aSymbol}/market_chart/range?vs_currency=${this.baseCurrency.toLowerCase()}&from=${getUnixTime( + from + )}&to=${getUnixTime(to)}`, + 'GET', + 'json', + 200 + ); + const { prices } = await get(); + + const result: { + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + } = { + [aSymbol]: {} + }; + + for (const [timestamp, marketPrice] of prices) { + result[aSymbol][format(fromUnixTime(timestamp / 1000), DATE_FORMAT)] = { + marketPrice + }; + } + + return result; + } catch (error) { + throw new Error( + `Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format( + from, + DATE_FORMAT + )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}` + ); + } + } + + public getMaxNumberOfSymbolsPerRequest() { + return 50; + } + + public getName(): DataSource { + return DataSource.COINGECKO; + } + + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + const results: { [symbol: string]: IDataProviderResponse } = {}; + + if (aSymbols.length <= 0) { + return {}; + } + + try { + const get = bent( + `${this.URL}/simple/price?ids=${aSymbols.join( + ',' + )}&vs_currencies=${this.baseCurrency.toLowerCase()}`, + 'GET', + 'json', + 200 + ); + const response = await get(); + + for (const symbol in response) { + if (Object.prototype.hasOwnProperty.call(response, symbol)) { + results[symbol] = { + currency: this.baseCurrency, + dataProviderInfo: this.getDataProviderInfo(), + dataSource: DataSource.COINGECKO, + marketPrice: response[symbol][this.baseCurrency.toLowerCase()], + marketState: 'open' + }; + } + } + } catch (error) { + Logger.error(error, 'CoinGeckoService'); + } + + return results; + } + + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { + let items: LookupItem[] = []; + + if (aQuery.length <= 2) { + return { items }; + } + + try { + const get = bent( + `${this.URL}/search?query=${aQuery}`, + 'GET', + 'json', + 200 + ); + const { coins } = await get(); + + items = coins.map(({ id: symbol, name }) => { + return { + name, + symbol, + currency: this.baseCurrency, + dataSource: this.getName() + }; + }); + } catch (error) { + Logger.error(error, 'CoinGeckoService'); + } + + return { items }; + } + + private getDataProviderInfo(): DataProviderInfo { + return { + name: 'CoinGecko', + url: 'https://coingecko.com' + }; + } +} diff --git a/apps/api/src/services/data-provider/data-provider.module.ts b/apps/api/src/services/data-provider/data-provider.module.ts index abafe6189..d36a12aa6 100644 --- a/apps/api/src/services/data-provider/data-provider.module.ts +++ b/apps/api/src/services/data-provider/data-provider.module.ts @@ -1,6 +1,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; +import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service'; import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider/eod-historical-data/eod-historical-data.service'; import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; @@ -21,6 +22,7 @@ import { DataProviderService } from './data-provider.service'; ], providers: [ AlphaVantageService, + CoinGeckoService, DataProviderService, EodHistoricalDataService, GoogleSheetsService, @@ -30,6 +32,7 @@ import { DataProviderService } from './data-provider.service'; { inject: [ AlphaVantageService, + CoinGeckoService, EodHistoricalDataService, GoogleSheetsService, ManualService, @@ -39,6 +42,7 @@ import { DataProviderService } from './data-provider.service'; provide: 'DataProviderInterfaces', useFactory: ( alphaVantageService, + coinGeckoService, eodHistoricalDataService, googleSheetsService, manualService, @@ -46,6 +50,7 @@ import { DataProviderService } from './data-provider.service'; yahooFinanceService ) => [ alphaVantageService, + coinGeckoService, eodHistoricalDataService, googleSheetsService, manualService, diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index 1148dd6af..58f25243c 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -1,4 +1,4 @@ -import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { DataProviderInfo, UniqueAsset } from '@ghostfolio/common/interfaces'; import { MarketState } from '@ghostfolio/common/types'; import { Account, @@ -28,6 +28,7 @@ export interface IDataProviderHistoricalResponse { export interface IDataProviderResponse { currency: string; + dataProviderInfo?: DataProviderInfo; dataSource: DataSource; marketPrice: number; marketState: MarketState; diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 485128e33..b3623e339 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -123,6 +123,13 @@ const routes: Routes = [ './pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.module' ).then((m) => m.GhostfolioAufSackgeldVorgestelltPageModule) }, + { + path: 'blog/2023/02/ghostfolio-meets-umbrel', + loadChildren: () => + import( + './pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module' + ).then((m) => m.GhostfolioMeetsUmbrelPageModule) + }, { path: 'demo', loadChildren: () => diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index aa2880d81..9869ad453 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -13,6 +13,7 @@ import { import { DataService } from '@ghostfolio/client/services/data.service'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { + DataProviderInfo, EnhancedSymbolProfile, LineChartItem } from '@ghostfolio/common/interfaces'; @@ -40,6 +41,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { public countries: { [code: string]: { name: string; value: number }; }; + public dataProviderInfo: DataProviderInfo; public dividendInBaseCurrency: number; public feeInBaseCurrency: number; public firstBuyDate: string; @@ -83,6 +85,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { .subscribe( ({ averagePrice, + dataProviderInfo, dividendInBaseCurrency, feeInBaseCurrency, firstBuyDate, @@ -105,6 +108,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.averagePrice = averagePrice; this.benchmarkDataItems = []; this.countries = {}; + this.dataProviderInfo = dataProviderInfo; this.dividendInBaseCurrency = dividendInBaseCurrency; this.feeInBaseCurrency = feeInBaseCurrency; this.firstBuyDate = firstBuyDate; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 33aebd8f3..7b009d85e 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -227,6 +227,12 @@
+
+
+ + +
+
@@ -249,7 +255,7 @@
-
+
Tags
{{ tag.name }} @@ -261,7 +267,7 @@ *ngIf="data.hasPermissionToReportDataGlitch === true && orders?.length > 0" class="row" > -
+

+
+
+
+
+

Ghostfolio meets Umbrel

+
2023-02-25
+ Ghostfolio meets Umbrel Teaser +
+
+

+ We are happy to announce that + Ghostfolio, the web-based personal + finance management software, is now available in the + Umbrel App Store, a + home server OS for self-hosting. +

+

+ In recent years, we have seen an increasing number of individuals + and organizations moving their data to the cloud. While cloud + computing has its benefits, such as accessibility and scalability, + it also comes with some concerns regarding data privacy and + security. However, there is an alternative to cloud computing that + provides the convenience of the cloud while giving you ownership and + control of your data: personal servers. +

+
+
+

Umbrel – A personal server OS for self-hosting

+

+ Umbrel + is an operating system based on + Docker that + allows you to run a personal server in your home. With it, you can + self-host open source apps directly from an integrated app store. + This means that you can discover self-hosted apps directly in the + Umbrel App Store + and install them in one click. You can get up and running Umbrel on + a Raspberry Pi 4, any Ubuntu / Debian system, or a VPS in only 5 + minutes. +

+

+ Umbrel offers numerous advantages for running a personal server in + your home, such as enhanced data privacy and security, ownership and + control of your data, and access to a diverse selection of + self-hosted apps. +

+
+
+

+ Ghostfolio – Track your portfolio without being tracked +

+

+ Keeping track of multiple assets can make managing your personal + finance a challenging task. However, there are tools available + beyond spreadsheets that can help you streamline the process and + make well-informed investment decisions based on data. +

+

+ Ghostfolio + is a modern open source web application designed to manage your + personal finance with ease and confidence. It presents your current + assets in real-time, including stocks, ETFs, cryptocurrencies, + commodities, and more. It allows you to track and analyze your + investments in one place. +

+

+ The application has a range of features such as real-time asset + tracking, data import and export and advanced portfolio analytics + tools. +

+
+
+

+ To participate in the ongoing development of Ghostfolio, please feel + free to reach out to us on our + Slack channel + or via Twitter + @ghostfolio_. We look forward to hearing from you! +

+
+
+
    +
  • + Announcement +
  • +
  • + App Store +
  • +
  • + Assets +
  • +
  • + Cloud +
  • +
  • + Commodity +
  • +
  • + Cryptocurrency +
  • +
  • + Debian +
  • +
  • + Development +
  • +
  • + Docker +
  • +
  • + ETF +
  • +
  • + Fintech +
  • +
  • + Ghostfolio +
  • +
  • + Home Server +
  • +
  • + Investing +
  • +
  • + Linux +
  • +
  • + Open Source +
  • +
  • + Operating System +
  • +
  • + OS +
  • +
  • + OSS +
  • +
  • + Personal Finance +
  • +
  • + Personal Server +
  • +
  • + Portfolio +
  • +
  • + Privacy +
  • +
  • + Raspberry Pi +
  • +
  • + Security +
  • +
  • + Software +
  • +
  • + Spreadsheet +
  • +
  • + Stocks +
  • +
  • + Ubuntu +
  • +
  • + Umbrel +
  • +
  • + VPS +
  • +
  • + Wealth Management +
  • +
+
+
+
+
+
diff --git a/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module.ts b/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module.ts new file mode 100644 index 000000000..b15ca1c2c --- /dev/null +++ b/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { GhostfolioMeetsUmbrelPageRoutingModule } from './ghostfolio-meets-umbrel-page-routing.module'; +import { GhostfolioMeetsUmbrelPageComponent } from './ghostfolio-meets-umbrel-page.component'; + +@NgModule({ + declarations: [GhostfolioMeetsUmbrelPageComponent], + imports: [CommonModule, GhostfolioMeetsUmbrelPageRoutingModule, RouterModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GhostfolioMeetsUmbrelPageModule {} diff --git a/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.scss b/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/pages/blog/blog-page.html b/apps/client/src/app/pages/blog/blog-page.html index 69b5000b9..0349eb168 100644 --- a/apps/client/src/app/pages/blog/blog-page.html +++ b/apps/client/src/app/pages/blog/blog-page.html @@ -2,6 +2,32 @@

Blog

+ + + + +
diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index cb2fbd649..4ba24f77a 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -165,6 +165,14 @@ title="Product Hunt – The best new products in tech." >
+
+ +
+
+ +
{{ + dataProviderInfo.name + }}, . + diff --git a/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.scss b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.scss new file mode 100644 index 000000000..d732e2f02 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.scss @@ -0,0 +1,7 @@ +:host { + display: block; + + a { + color: rgba(var(--palette-primary-500), 1); + } +} diff --git a/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.ts b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.ts new file mode 100644 index 000000000..1da2fcfc9 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/data-provider-credits.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { DataProviderInfo } from '@ghostfolio/common/interfaces'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-data-provider-credits', + styleUrls: ['./data-provider-credits.component.scss'], + templateUrl: './data-provider-credits.component.html' +}) +export class DataProviderCreditsComponent { + @Input() dataProviderInfos: DataProviderInfo[]; + + public constructor() {} +} diff --git a/libs/ui/src/lib/data-provider-credits/data-provider-credits.module.ts b/libs/ui/src/lib/data-provider-credits/data-provider-credits.module.ts new file mode 100644 index 000000000..e5dd9d3b9 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/data-provider-credits.module.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; + +import { DataProviderCreditsComponent } from './data-provider-credits.component'; + +@NgModule({ + declarations: [DataProviderCreditsComponent], + exports: [DataProviderCreditsComponent], + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfDataProviderCreditsModule {} diff --git a/libs/ui/src/lib/data-provider-credits/index.ts b/libs/ui/src/lib/data-provider-credits/index.ts new file mode 100644 index 000000000..5d3759577 --- /dev/null +++ b/libs/ui/src/lib/data-provider-credits/index.ts @@ -0,0 +1 @@ +export * from './data-provider-credits.module'; diff --git a/package.json b/package.json index 0b29d036c..354ed918c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.237.0", + "version": "1.238.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { @@ -43,7 +43,7 @@ "start:prod": "yarn database:migrate && yarn database:seed && node main", "start:server": "nx run api:serve --watch", "start:storybook": "nx run ui:storybook", - "test": "nx test", + "test": "npx dotenv-cli -e .env.example -- nx test", "test:single": "nx test --test-file portfolio-calculator-novn-buy-and-sell.spec.ts", "ts-node": "ts-node", "update": "nx migrate latest", @@ -127,7 +127,7 @@ "twitter-api-v2": "1.10.3", "uuid": "9.0.0", "yahoo-finance2": "2.3.10", - "zone.js": "0.11.8" + "zone.js": "0.12.0" }, "devDependencies": { "@angular-devkit/build-angular": "15.1.6", diff --git a/prisma/migrations/20230222200407_added_coingecko_to_data_source/migration.sql b/prisma/migrations/20230222200407_added_coingecko_to_data_source/migration.sql new file mode 100644 index 000000000..2b40d220a --- /dev/null +++ b/prisma/migrations/20230222200407_added_coingecko_to_data_source/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "DataSource" ADD VALUE 'COINGECKO'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 937897a1d..dc4c1302d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -205,6 +205,7 @@ enum AssetSubClass { enum DataSource { ALPHA_VANTAGE + COINGECKO EOD_HISTORICAL_DATA GOOGLE_SHEETS MANUAL diff --git a/yarn.lock b/yarn.lock index 5384065f9..54c207add 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21932,10 +21932,10 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zone.js@0.11.8: - version "0.11.8" - resolved "https://registry.npmjs.org/zone.js/-/zone.js-0.11.8.tgz" - integrity sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA== +zone.js@0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.12.0.tgz#a4a6e5fab6d34bd37d89c77e89ac2e6f4a3d2c30" + integrity sha512-XtC+I5dXU14HrzidAKBNMqneIVUykLEAA1x+v4KVrd6AUPWlwYORF8KgsVqvgdHiKZ4BkxxjvYi/ksEixTPR0Q== dependencies: tslib "^2.3.0"