Compare commits

..

25 Commits

Author SHA1 Message Date
127abb8f4e Release 1.93.0 (#566) 2021-12-21 20:24:24 +01:00
ed1136999a Feature/extend documentation for self hosting (#565)
* Extend documentation for self-hosting

* Add tag "latest" to docker image

* Update changelog
2021-12-21 20:22:58 +01:00
9f545e3e2b Feature/add cryptocurrency solana (#563)
* Add support for Solana and clean up symbol conversion (SOL1USD)

* Update changelog
2021-12-20 21:24:58 +01:00
1602f976f0 Feature/convert errors to warnings in portfolio calculator (#562)
* Convert errors to warnings

* Update changelog
2021-12-20 21:03:12 +01:00
4bf4c1a8a3 Release 1.92.0 (#561) 2021-12-19 16:57:22 +01:00
e78755c280 Replace OrderType with Type (prisma) (#560) 2021-12-19 16:52:35 +01:00
7772684413 Bugfix/fix permission for system status page (#559)
* Fix permission

* Update changelog
2021-12-19 15:55:03 +01:00
955302666e Bugfix/improve redirection on logout (#558)
* Improve redirection on logout

* Update changelog
2021-12-19 13:45:28 +01:00
ddce8cc7f9 Feature/support update of historical data (#557)
* Support update of historical data

* Update changelog
2021-12-19 10:57:50 +01:00
aca0d77e91 Feature/add line chart to historical data view (#555)
* Add line chart

* Update changelog
2021-12-18 16:43:34 +01:00
8b9379f5ce Release 1.91.0 (#553) 2021-12-18 11:41:13 +01:00
0806d0dc92 Bugfix/fix hidden statistics values (#552)
* Fix hidden values (0)

* Update changelog
2021-12-18 11:13:32 +01:00
e518bc3779 Feature/clean up all time high and low from performance endpoint (#551)
* Remove isAllTimeHigh / isAllTimeLow

* Update changelog
2021-12-18 09:20:42 +01:00
eff807dd9a Add tests for yahoo Finance symbol conversion (#550)
* Add tests for yahoo Finance symbol conversion

* Refactoring
2021-12-18 09:04:52 +01:00
155bf67f60 Bugfix/fix symbol conversion from yahoo finance with hyphen (#549)
* Fix symbol conversion from Yahoo Finance

* Update changelog
2021-12-16 18:44:08 +01:00
9aefe3747e Feature/migrate database schema to prisma 3 (#546)
* Add migration after upgrade to prisma 3

* Update changelog
2021-12-15 21:56:29 +01:00
0878febded build(prisma): fix populating database when attributes have null value (#545) 2021-12-15 21:04:26 +01:00
f953c6ea64 Release/1.90.0 (#544)
* Update stage labels

* Release 1.90.0
2021-12-14 20:04:17 +01:00
76dbf78279 implement docker build on tags (#542)
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>

Co-authored-by: Valentin Zickner <github@zickner.ch>
2021-12-14 19:45:55 +01:00
f12866b9ec Update changelog (#543) 2021-12-13 22:31:49 +01:00
83ba5f3d9f Feature/add intro image for dark mode (#540)
* Add intro image for dark mode

* Update changelog
2021-12-13 22:26:16 +01:00
7439c1bf54 fix(custom-cryptocurrencies): add uniswap (uni3) (#541) 2021-12-13 22:25:09 +01:00
e255b76053 Feature/check currency in import validation (#538)
* Check currency in import validation (avoid importing transactions in wrong currency)

* Update changelog
2021-12-13 19:45:23 +01:00
ed7209fb53 Feature/remove transactions import limit (#536)
* Remove default transactions import limit

* Update changelog
2021-12-11 21:44:54 +01:00
008a2ab123 Support arm64 (#465)
* Support arm64

* Move prisma from devDependencies to dependencies (Fix "/bin/sh: prisma: not found" in docker build)

* Update changelog

Co-authored-by: Valentin Zickner <ghostfolio@zickner.ch>
2021-12-11 20:58:09 +01:00
48 changed files with 645 additions and 190 deletions

View File

@ -3,9 +3,28 @@ git:
depth: false
node_js:
- 14
before_script:
- yarn
script:
- yarn format:check
- yarn test
- yarn build:all
services:
- docker
cache: yarn
if: (type = pull_request) OR (tag IS present)
jobs:
include:
- stage: Install dependencies
if: type = pull_request
script: yarn --frozen-lockfile
- stage: Check formatting
if: type = pull_request
script: yarn format:check
- stage: Execute tests
if: type = pull_request
script: yarn test
- stage: Build application
if: type = pull_request
script: yarn build:all
- stage: Build and publish docker image
if: tag IS present
script: ./publish-docker-image.sh

View File

@ -5,6 +5,62 @@ 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).
## 1.93.0 - 21.12.2021
### Added
- Added support for cryptocurrency _Solana_ (`SOL-USD`)
- Extended the documentation for self-hosting with the [official Ghostfolio Docker image](https://hub.docker.com/r/ghostfolio/ghostfolio)
### Fixed
- Converted errors to warnings in portfolio calculator
## 1.92.0 - 19.12.2021
### Added
- Added a line chart to the historical data view in the admin control panel
- Supported the update of historical data in the admin control panel
### Fixed
- Improved the redirection on logout
- Fixed the permission for the system status page
## 1.91.0 - 18.12.2021
### Changed
- Removed the redundant all time high and all time low from the performance endpoint
### Fixed
- Fixed the symbol conversion from _Yahoo Finance_ including a hyphen
- Fixed hidden values (`0`) in the statistics section on the about page
### Todo
- Apply data migration (`yarn database:migrate`)
## 1.90.0 - 14.12.2021
### Added
- Extended the validation in the import functionality for transactions by checking the currency of the data provider service
- Added support for cryptocurrency _Uniswap_
- Set up pipeline for docker build
### Changed
- Removed the default transactions import limit
- Improved the landing page in dark mode
### Fixed
- Fixed `/bin/sh: prisma: not found` in docker build
- Added `apk` in `Dockerfile` (`python3 g++ make openssl`)
## 1.89.0 - 11.12.2021
### Added

View File

@ -12,7 +12,8 @@ COPY ./package.json package.json
COPY ./yarn.lock yarn.lock
COPY ./prisma/schema.prisma prisma/schema.prisma
RUN yarn
RUN apk add --no-cache python3 g++ make openssl
RUN yarn install
# See https://github.com/nrwl/nx/issues/6586 for further details
COPY ./decorate-angular-cli.js decorate-angular-cli.js

View File

@ -81,19 +81,27 @@ The backend is based on [NestJS](https://nestjs.com) using [PostgreSQL](https://
The frontend is built with [Angular](https://angular.io) and uses [Angular Material](https://material.angular.io) with utility classes from [Bootstrap](https://getbootstrap.com).
## Run with Docker
## Run with Docker (self-hosting)
### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop)
### Setup Docker Image
### a. Run environment
Run the following commands to build and start the Docker image:
Run the following command to start the Docker images from [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio):
```bash
docker-compose -f docker/docker-compose-build-local.yml build
docker-compose -f docker/docker-compose-build-local.yml up
docker-compose -f docker/docker-compose.yml up
```
### b. Build and run environment
Run the following commands to build and start the Docker images:
```bash
docker-compose -f docker/docker-compose.build.yml build
docker-compose -f docker/docker-compose.build.yml up
```
### Setup Database
@ -112,6 +120,12 @@ Open http://localhost:3333 in your browser and accomplish these steps:
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
1. Click _Sign out_ and check out the _Live Demo_
### Finalization
1. Create a new user via _Get Started_
1. Assign the role `ADMIN` to this user (directly in the database)
1. Delete the original _Admin_ (directly in the database)
### Migrate Database
With the following command you can keep your database schema in sync after a Ghostfolio version update:

View File

@ -1,6 +1,6 @@
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
AdminData,
AdminMarketData,
@ -22,16 +22,18 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource, MarketData } from '@prisma/client';
import { isDate, isValid } from 'date-fns';
import { isDate } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service';
import { UpdateMarketDataDto } from './update-market-data.dto';
@Controller('admin')
export class AdminController {
public constructor(
private readonly adminService: AdminService,
private readonly dataGatheringService: DataGatheringService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@ -173,7 +175,7 @@ export class AdminController {
@Get('market-data/:symbol')
@UseGuards(AuthGuard('jwt'))
public async getMarketDataBySymbol(
@Param('symbol') symbol
@Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> {
if (
!hasPermission(
@ -190,6 +192,39 @@ export class AdminController {
return this.adminService.getMarketDataBySymbol(symbol);
}
@Put('market-data/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt'))
public async update(
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string,
@Body() data: UpdateMarketDataDto
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const date = new Date(dateString);
return this.marketDataService.updateMarketData({
data,
where: {
date_symbol: {
date,
symbol
}
}
});
}
@Put('settings/:key')
@UseGuards(AuthGuard('jwt'))
public async updateProperty(

View File

@ -0,0 +1,6 @@
import { IsNumber } from 'class-validator';
export class UpdateMarketDataDto {
@IsNumber()
marketPrice: number;
}

View File

@ -1,4 +1,5 @@
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { Injectable } from '@nestjs/common';
import { Order } from '@prisma/client';
@ -6,9 +7,8 @@ import { isSameDay, parseISO } from 'date-fns';
@Injectable()
export class ImportService {
private static MAX_ORDERS_TO_IMPORT = 20;
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
private readonly orderService: OrderService
) {}
@ -59,8 +59,14 @@ export class ImportService {
orders: Partial<Order>[];
userId: string;
}) {
if (orders?.length > ImportService.MAX_ORDERS_TO_IMPORT) {
throw new Error('Too many transactions');
if (
orders?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT')
) {
throw new Error(
`Too many transactions (${this.configurationService.get(
'MAX_ORDERS_TO_IMPORT'
)} at most)`
);
}
const existingOrders = await this.orderService.orders({
@ -98,6 +104,12 @@ export class ImportService {
`orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
);
}
if (result[symbol].currency !== currency) {
throw new Error(
`orders.${index}.currency ("${currency}") does not match with "${result[symbol].currency}"`
);
}
}
}
}

View File

@ -1,5 +1,4 @@
import { OrderType } from '@ghostfolio/api/models/order-type';
import { DataSource } from '@prisma/client';
import { DataSource, Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js';
export interface PortfolioOrder {
@ -10,6 +9,6 @@ export interface PortfolioOrder {
name: string;
quantity: Big;
symbol: string;
type: OrderType;
type: TypeOfOrder;
unitPrice: Big;
}

View File

@ -1,4 +1,3 @@
import { OrderType } from '@ghostfolio/api/models/order-type';
import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { DataSource } from '@prisma/client';
import Big from 'big.js';
@ -155,7 +154,7 @@ describe('PortfolioCalculator', () => {
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('144.38'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -166,7 +165,7 @@ describe('PortfolioCalculator', () => {
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('147.99'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -177,7 +176,7 @@ describe('PortfolioCalculator', () => {
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('15'),
symbol: 'VTI',
type: OrderType.Sell,
type: 'SELL',
unitPrice: new Big('151.41'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -248,7 +247,7 @@ describe('PortfolioCalculator', () => {
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('144.38'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -259,7 +258,7 @@ describe('PortfolioCalculator', () => {
name: 'Something else',
quantity: new Big('10'),
symbol: 'VTX',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('147.99'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -270,7 +269,7 @@ describe('PortfolioCalculator', () => {
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('5'),
symbol: 'VTI',
type: OrderType.Sell,
type: 'SELL',
unitPrice: new Big('151.41'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -360,7 +359,7 @@ describe('PortfolioCalculator', () => {
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('20'),
symbol: 'VTI',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('197.15'),
fee: new Big(0)
}
@ -462,7 +461,7 @@ describe('PortfolioCalculator', () => {
name: 'Amazon.com, Inc.',
quantity: new Big('5'),
symbol: 'AMZN',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('2021.99'),
fee: new Big(0)
}
@ -617,7 +616,7 @@ describe('PortfolioCalculator', () => {
name: 'Amazon.com, Inc.',
quantity: new Big('5'),
symbol: 'AMZN',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('2021.99'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -628,7 +627,7 @@ describe('PortfolioCalculator', () => {
name: 'Amazon.com, Inc.',
quantity: new Big('5'),
symbol: 'AMZN',
type: OrderType.Sell,
type: 'SELL',
unitPrice: new Big('2412.23'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -2391,7 +2390,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
name: 'Tesla, Inc.',
quantity: new Big('50'),
symbol: 'TSLA',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('42.97'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -2402,7 +2401,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
name: 'Bitcoin USD',
quantity: new Big('0.5614682'),
symbol: 'BTCUSD',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('3562.089535970158'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -2413,7 +2412,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
name: 'Amazon.com, Inc.',
quantity: new Big('5'),
symbol: 'AMZN',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('2021.99'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -2427,7 +2426,7 @@ const ordersVTI: PortfolioOrder[] = [
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('144.38'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -2438,7 +2437,7 @@ const ordersVTI: PortfolioOrder[] = [
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('147.99'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -2449,7 +2448,7 @@ const ordersVTI: PortfolioOrder[] = [
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('15'),
symbol: 'VTI',
type: OrderType.Sell,
type: 'SELL',
unitPrice: new Big('151.41'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -2460,7 +2459,7 @@ const ordersVTI: PortfolioOrder[] = [
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('177.69'),
currency: 'USD',
dataSource: DataSource.YAHOO,
@ -2471,7 +2470,7 @@ const ordersVTI: PortfolioOrder[] = [
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
type: 'BUY',
unitPrice: new Big('203.15'),
currency: 'USD',
dataSource: DataSource.YAHOO,

View File

@ -1,9 +1,9 @@
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
import { OrderType } from '@ghostfolio/api/models/order-type';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { TimelinePosition } from '@ghostfolio/common/interfaces';
import { Logger } from '@nestjs/common';
import { Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js';
import {
addDays,
@ -238,9 +238,7 @@ export class PortfolioCalculator {
if (!marketSymbolMap[nextDate]?.[item.symbol]) {
invalidSymbols.push(item.symbol);
hasErrors = true;
Logger.error(
`Missing value for symbol ${item.symbol} at ${nextDate}`
);
Logger.warn(`Missing value for symbol ${item.symbol} at ${nextDate}`);
continue;
}
let lastInvestment: Big = new Big(0);
@ -271,7 +269,7 @@ export class PortfolioCalculator {
if (!initialValue) {
invalidSymbols.push(item.symbol);
hasErrors = true;
Logger.error(
Logger.warn(
`Missing value for symbol ${item.symbol} at ${currentDate}`
);
continue;
@ -515,7 +513,7 @@ export class PortfolioCalculator {
currentPosition.netPerformancePercentage.mul(currentInitialValue)
);
} else if (!currentPosition.quantity.eq(0)) {
Logger.error(
Logger.warn(
`Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}`
);
hasErrors = true;
@ -660,14 +658,14 @@ export class PortfolioCalculator {
};
}
private getFactor(type: OrderType) {
private getFactor(type: TypeOfOrder) {
let factor: number;
switch (type) {
case OrderType.Buy:
case 'BUY':
factor = 1;
break;
case OrderType.Sell:
case 'SELL':
factor = -1;
break;
default:

View File

@ -6,7 +6,6 @@ import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfol
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator';
import { OrderType } from '@ghostfolio/api/models/order-type';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
@ -21,11 +20,7 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import {
UNKNOWN_KEY,
baseCurrency,
ghostfolioCashSymbol
} from '@ghostfolio/common/config';
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import {
Accounts,
@ -413,7 +408,7 @@ export class PortfolioService {
name: order.SymbolProfile?.name,
quantity: new Big(order.quantity),
symbol: order.symbol,
type: <OrderType>order.type,
type: order.type,
unitPrice: new Big(order.unitPrice)
}));
@ -693,9 +688,7 @@ export class PortfolioService {
currentGrossPerformancePercent: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentValue: 0,
isAllTimeHigh: false,
isAllTimeLow: false
currentValue: 0
}
};
}
@ -728,9 +721,7 @@ export class PortfolioService {
currentGrossPerformancePercent,
currentNetPerformance,
currentNetPerformancePercent,
currentValue,
isAllTimeHigh: true,
isAllTimeLow: false
currentValue
}
};
}
@ -854,8 +845,8 @@ export class PortfolioService {
const fees = this.getFees(orders);
const firstOrderDate = orders[0]?.date;
const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
const totalBuy = this.getTotalByType(orders, currency, 'BUY');
const totalSell = this.getTotalByType(orders, currency, 'SELL');
const committedFunds = new Big(totalBuy).sub(totalSell);
@ -994,7 +985,7 @@ export class PortfolioService {
name: order.SymbolProfile?.name,
quantity: new Big(order.quantity),
symbol: order.symbol,
type: <OrderType>order.type,
type: order.type,
unitPrice: new Big(
this.exchangeRateDataService.toCurrency(
order.unitPrice,

View File

@ -1,3 +1,4 @@
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
@ -12,9 +13,9 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { DataSource, MarketData } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isEmpty } from 'lodash';
import { isDate, isEmpty } from 'lodash';
import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@ -78,4 +79,27 @@ export class SymbolController {
return result;
}
@Get(':dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt'))
public async gatherSymbolForDate(
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
const date = new Date(dateString);
if (!isDate(date)) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
return this.symbolService.getForDate({
dataSource,
date,
symbol
});
}
}

View File

@ -1,11 +1,15 @@
import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import {
IDataGatheringItem,
IDataProviderHistoricalResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { subDays } from 'date-fns';
import { DataSource, MarketData } from '@prisma/client';
import { format, subDays } from 'date-fns';
import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@ -58,6 +62,27 @@ export class SymbolService {
return undefined;
}
public async getForDate({
dataSource,
date,
symbol
}: {
dataSource: DataSource;
date: Date;
symbol: string;
}): Promise<IDataProviderHistoricalResponse> {
const historicalData = await this.dataProviderService.getHistoricalRaw(
[{ dataSource, symbol }],
date,
date
);
return {
marketPrice:
historicalData?.[symbol]?.[format(date, DATE_FORMAT)]?.marketPrice
};
}
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {
const results: { items: LookupItem[] } = { items: [] };

View File

@ -1,8 +0,0 @@
export enum OrderType {
CorporateAction = 'CORPORATE_ACTION',
Bonus = 'BONUS',
Buy = 'BUY',
Dividend = 'DIVIDEND',
Sell = 'SELL',
Split = 'SPLIT'
}

View File

@ -1,8 +1,7 @@
import { Account, SymbolProfile } from '@prisma/client';
import { Account, SymbolProfile, Type as TypeOfOrder } from '@prisma/client';
import { v4 as uuidv4 } from 'uuid';
import { IOrder } from '../services/interfaces/interfaces';
import { OrderType } from './order-type';
export class Order {
private account: Account;
@ -15,7 +14,7 @@ export class Order {
private symbol: string;
private symbolProfile: SymbolProfile;
private total: number;
private type: OrderType;
private type: TypeOfOrder;
private unitPrice: number;
public constructor(data: IOrder) {

View File

@ -27,6 +27,7 @@ export class ConfigurationService {
GOOGLE_SECRET: str({ default: 'dummySecret' }),
JWT_SECRET_KEY: str({}),
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
MAX_ORDERS_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
PORT: port({ default: 3333 }),
RAKUTEN_RAPID_API_KEY: str({ default: '' }),
REDIS_HOST: str({ default: 'localhost' }),

View File

@ -10,7 +10,7 @@ export class CryptocurrencyService {
public constructor() {}
public isCrypto(aSymbol = '') {
public isCryptocurrency(aSymbol = '') {
const cryptocurrencySymbol = aSymbol.substring(0, aSymbol.length - 3);
return this.getCryptocurrencies().includes(cryptocurrencySymbol);
}

View File

@ -3,5 +3,7 @@
"ALGO": "Algorand",
"AVAX": "Avalanche",
"MATIC": "Polygon",
"SHIB": "Shiba Inu"
"SHIB": "Shiba Inu",
"SOL": "Solana",
"UNI3": "Uniswap"
}

View File

@ -0,0 +1,60 @@
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { YahooFinanceService } from './yahoo-finance.service';
jest.mock(
'@ghostfolio/api/services/cryptocurrency/cryptocurrency.service',
() => {
return {
CryptocurrencyService: jest.fn().mockImplementation(() => {
return {
isCryptocurrency: (symbol: string) => {
switch (symbol) {
case 'BTCUSD':
return true;
case 'DOGEUSD':
return true;
default:
return false;
}
}
};
})
};
}
);
describe('YahooFinanceService', () => {
let cryptocurrencyService: CryptocurrencyService;
let yahooFinanceService: YahooFinanceService;
beforeAll(async () => {
cryptocurrencyService = new CryptocurrencyService();
yahooFinanceService = new YahooFinanceService(cryptocurrencyService);
});
it('convertFromYahooFinanceSymbol', async () => {
expect(
await yahooFinanceService.convertFromYahooFinanceSymbol('BRK-B')
).toEqual('BRK-B');
expect(
await yahooFinanceService.convertFromYahooFinanceSymbol('BTC-USD')
).toEqual('BTCUSD');
expect(
await yahooFinanceService.convertFromYahooFinanceSymbol('EURUSD=X')
).toEqual('EURUSD');
});
it('convertToYahooFinanceSymbol', async () => {
expect(
await yahooFinanceService.convertToYahooFinanceSymbol('BTCUSD')
).toEqual('BTC-USD');
expect(
await yahooFinanceService.convertToYahooFinanceSymbol('DOGEUSD')
).toEqual('DOGE-USD');
expect(
await yahooFinanceService.convertToYahooFinanceSymbol('USDCHF')
).toEqual('USDCHF=X');
});
});

View File

@ -1,6 +1,6 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
@ -35,6 +35,44 @@ export class YahooFinanceService implements DataProviderInterface {
return true;
}
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
const symbol = aYahooFinanceSymbol.replace(
new RegExp(`-${baseCurrency}$`),
baseCurrency
);
return symbol.replace('=X', '');
}
/**
* Converts a symbol to a Yahoo Finance symbol
*
* Currency: USDCHF -> USDCHF=X
* Cryptocurrency: BTCUSD -> BTC-USD
* DOGEUSD -> DOGE-USD
*/
public convertToYahooFinanceSymbol(aSymbol: string) {
if (aSymbol.includes(baseCurrency) && aSymbol.length >= 6) {
if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) {
return `${aSymbol}=X`;
} else if (
this.cryptocurrencyService.isCryptocurrency(
aSymbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency)
)
) {
// Add a dash before the last three characters
// BTCUSD -> BTC-USD
// DOGEUSD -> DOGE-USD
// SOL1USD -> SOL1-USD
return aSymbol.replace(
new RegExp(`-?${baseCurrency}$`),
`-${baseCurrency}`
);
}
}
return aSymbol;
}
public async get(
aSymbols: string[]
): Promise<{ [symbol: string]: IDataProviderResponse }> {
@ -69,7 +107,7 @@ export class YahooFinanceService implements DataProviderInterface {
exchange: this.parseExchange(value.price?.exchangeName),
marketState:
value.price?.marketState === 'REGULAR' ||
this.cryptocurrencyService.isCrypto(symbol)
this.cryptocurrencyService.isCryptocurrency(symbol)
? MarketState.open
: MarketState.closed,
marketPrice: value.price?.regularMarketPrice || 0,
@ -204,8 +242,8 @@ export class YahooFinanceService implements DataProviderInterface {
.filter(({ quoteType, symbol }) => {
return (
(quoteType === 'CRYPTOCURRENCY' &&
this.cryptocurrencyService.isCrypto(
symbol.replace(new RegExp('-USD$'), 'USD').replace('1', '')
this.cryptocurrencyService.isCryptocurrency(
symbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency)
)) ||
quoteType === 'EQUITY' ||
quoteType === 'ETF'
@ -213,9 +251,9 @@ export class YahooFinanceService implements DataProviderInterface {
})
.filter(({ quoteType, symbol }) => {
if (quoteType === 'CRYPTOCURRENCY') {
// Only allow cryptocurrencies in USD to avoid having redundancy in the database.
// Trades need to be converted manually before to USD (or a UI converter needs to be developed)
return symbol.includes('USD');
// Only allow cryptocurrencies in base currency to avoid having redundancy in the database.
// Transactions need to be converted manually to the base currency before
return symbol.includes(baseCurrency);
}
return true;
@ -239,44 +277,6 @@ export class YahooFinanceService implements DataProviderInterface {
return { items };
}
private convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
const symbol = aYahooFinanceSymbol.replace('-', '');
return symbol.replace('=X', '');
}
/**
* Converts a symbol to a Yahoo Finance symbol
*
* Currency: USDCHF -> USDCHF=X
* Cryptocurrency: BTCUSD -> BTC-USD
* DOGEUSD -> DOGE-USD
* SOL1USD -> SOL1-USD
*/
private convertToYahooFinanceSymbol(aSymbol: string) {
if (
(aSymbol.includes('CHF') ||
aSymbol.includes('EUR') ||
aSymbol.includes('USD')) &&
aSymbol.length >= 6
) {
if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) {
return `${aSymbol}=X`;
} else if (
this.cryptocurrencyService.isCrypto(
aSymbol.replace(new RegExp('-USD$'), 'USD').replace('1', '')
)
) {
// Add a dash before the last three characters
// BTCUSD -> BTC-USD
// DOGEUSD -> DOGE-USD
// SOL1USD -> SOL1-USD
return aSymbol.replace(new RegExp('-?USD$'), '-USD');
}
}
return aSymbol;
}
private parseAssetClass(aPrice: IYahooFinancePrice): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;

View File

@ -18,6 +18,7 @@ export interface Environment extends CleanedEnvAccessors {
GOOGLE_SECRET: string;
JWT_SECRET_KEY: string;
MAX_ITEM_IN_CACHE: number;
MAX_ORDERS_TO_IMPORT: number;
PORT: number;
RAKUTEN_RAPID_API_KEY: string;
REDIS_HOST: string;

View File

@ -3,11 +3,10 @@ import {
AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
SymbolProfile,
Type as TypeOfOrder
} from '@prisma/client';
import { OrderType } from '../../models/order-type';
export const MarketState = {
closed: 'closed',
delayed: 'delayed',
@ -24,7 +23,7 @@ export interface IOrder {
quantity: number;
symbol: string;
symbolProfile: SymbolProfile;
type: OrderType;
type: TypeOfOrder;
unitPrice: number;
}

View File

@ -65,4 +65,16 @@ export class MarketDataService {
where
});
}
public async updateMarketData(params: {
data: Prisma.MarketDataUpdateInput;
where: Prisma.MarketDataWhereUniqueInput;
}): Promise<MarketData> {
const { data, where } = params;
return this.prismaService.marketData.update({
data,
where
});
}
}

View File

@ -89,7 +89,7 @@ export class AppComponent implements OnDestroy, OnInit {
this.tokenStorageService.signOut();
this.userService.remove();
window.location.reload();
this.router.navigate(['/']);
}
public ngOnDestroy() {

View File

@ -1,4 +1,10 @@
<div class="py-2">
<div>
<gf-line-chart
class="mb-4"
[historicalDataItems]="historicalDataItems"
[showXAxis]="true"
[showYAxis]="true"
></gf-line-chart>
<div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex">
<div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>
<div class="align-items-center d-flex flex-grow-1 px-1">

View File

@ -14,6 +14,10 @@
margin-right: 0.25rem;
width: 0.5rem;
&:hover {
opacity: 0.8;
}
&.valid {
background-color: var(--danger);
}

View File

@ -1,13 +1,16 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
OnInit
OnInit,
Output
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { DataSource, MarketData } from '@prisma/client';
import { format, isBefore, isValid, parse } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector';
@ -26,9 +29,12 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
@Input() marketData: MarketData[];
@Input() symbol: string;
@Output() marketDataChanged = new EventEmitter<boolean>();
public days = Array(31);
public defaultDateFormat = DEFAULT_DATE_FORMAT;
public deviceType: string;
public historicalDataItems: LineChartItem[];
public marketDataByMonth: {
[yearMonth: string]: { [day: string]: MarketData & { day: number } };
} = {};
@ -45,6 +51,12 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
public ngOnInit() {}
public ngOnChanges() {
this.historicalDataItems = this.marketData.map((marketDataItem) => {
return {
date: format(marketDataItem.date, DATE_FORMAT),
value: marketDataItem.marketPrice
};
});
this.marketDataByMonth = {};
for (const marketDataItem of this.marketData) {
@ -93,7 +105,9 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
.subscribe(({ withRefresh }) => {
this.marketDataChanged.next(withRefresh);
});
}
public ngOnDestroy() {

View File

@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { AdminMarketDataDetailComponent } from './admin-market-data-detail.component';
import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/market-data-detail-dialog.module';
@ -7,7 +8,7 @@ import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/mark
@NgModule({
declarations: [AdminMarketDataDetailComponent],
exports: [AdminMarketDataDetailComponent],
imports: [CommonModule, GfMarketDataDetailDialogModule],
imports: [CommonModule, GfLineChartModule, GfMarketDataDetailDialogModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})

View File

@ -7,7 +7,6 @@ import {
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { MarketData } from '@prisma/client';
import { Subject, takeUntil } from 'rxjs';
import { MarketDataDetailDialogParams } from './interfaces/interfaces';
@ -32,24 +31,38 @@ export class MarketDataDetailDialog implements OnDestroy {
public ngOnInit() {}
public onCancel(): void {
this.dialogRef.close();
this.dialogRef.close({ withRefresh: false });
}
public onGatherData() {
public onFetchSymbolForDate() {
this.adminService
.gatherSymbol({
.fetchSymbolForDate({
dataSource: this.data.dataSource,
date: this.data.date,
symbol: this.data.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((marketData: MarketData) => {
this.data.marketPrice = marketData.marketPrice;
.subscribe(({ marketPrice }) => {
this.data.marketPrice = marketPrice;
this.changeDetectorRef.markForCheck();
});
}
public onUpdate() {
this.adminService
.putMarketData({
dataSource: this.data.dataSource,
date: this.data.date,
marketData: { marketPrice: this.data.marketPrice },
symbol: this.data.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.dialogRef.close({ withRefresh: true });
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

View File

@ -21,22 +21,30 @@
<mat-datepicker #date disabled="true"></mat-datepicker>
</mat-form-field>
</div>
<div class="align-items-center d-flex">
<mat-form-field appearance="outline" class="flex-grow-1 mr-2">
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Market Price</mat-label>
<input
matInput
name="marketPrice"
readonly
type="number"
[(ngModel)]="data.marketPrice"
/>
<button
mat-icon-button
matSuffix
title="Fetch market price"
(click)="onFetchSymbolForDate()"
>
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
</button>
</mat-form-field>
<button color="accent" i18n mat-flat-button (click)="onGatherData()">
Gather Data
</button>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button>
<button color="primary" i18n mat-flat-button (click)="onUpdate()">
Save
</button>
</div>
</form>

View File

@ -68,6 +68,13 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
}
}
public onMarketDataChanged(withRefresh: boolean = false) {
if (withRefresh) {
this.fetchAdminMarketData();
this.fetchAdminMarketDataBySymbol(this.currentSymbol);
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

View File

@ -4,7 +4,6 @@
<table class="gf-table w-100">
<thead>
<tr class="mat-header-row">
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th>
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
<th class="mat-header-cell px-1 py-2" i18n>First Transaction</th>
@ -17,7 +16,6 @@
class="cursor-pointer mat-row"
(click)="setCurrentSymbol(item.symbol)"
>
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
<td class="mat-cell px-1 py-2">{{ item.dataSource}}</td>
<td class="mat-cell px-1 py-2">
@ -44,12 +42,12 @@
</td>
</tr>
<tr *ngIf="currentSymbol === item.symbol" class="mat-row">
<td></td>
<td colspan="4">
<td class="p-1" colspan="4">
<gf-admin-market-data-detail
[dataSource]="item.dataSource"
[marketData]="marketDataDetails"
[symbol]="item.symbol"
(marketDataChanged)="onMarketDataChanged($event)"
></gf-admin-market-data-detail>
</td>
</tr>

View File

@ -5,7 +5,7 @@
<table class="gf-table">
<thead>
<tr class="mat-header-row">
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th>
<th class="mat-header-cell px-1 py-2 text-right">#</th>
<th class="mat-header-cell px-1 py-2" i18n>User</th>
<th class="mat-header-cell px-1 py-2 text-right" i18n>
Registration

View File

@ -23,7 +23,7 @@
This instance is running Ghostfolio {{ version }} and has been
last published on {{ lastPublish }}.
</ng-container>
<ng-container *ngIf="hasPermissionForSubscription" i18n
<ng-container *ngIf="hasPermissionForStatistics" i18n
>Check the system status at
<a href="https://status.ghostfol.io" title="Ghostfolio status"
>status.ghostfol.io</a
@ -108,8 +108,11 @@
<mat-card-content>
<div class="row">
<div class="col-xs-12 col-md-4 my-2">
<h3 class="mb-0" [hidden]="!statistics?.activeUsers1d">
{{ statistics?.activeUsers1d ?? '-' }}
<h3
class="mb-0"
[hidden]="statistics?.activeUsers1d === undefined"
>
{{ statistics?.activeUsers1d || '-' }}
</h3>
<div class="h6 mb-0">
<span i18n>Active Users</span>&nbsp;<small class="text-muted"
@ -118,7 +121,10 @@
</div>
</div>
<div class="col-xs-12 col-md-4 my-2">
<h3 class="mb-0" [hidden]="!statistics?.activeUsers7d">
<h3
class="mb-0"
[hidden]="statistics?.activeUsers7d === undefined"
>
{{ statistics?.activeUsers7d ?? '-' }}
</h3>
<div class="h6 mb-0">
@ -128,7 +134,10 @@
</div>
</div>
<div class="col-xs-12 col-md-4 my-2">
<h3 class="mb-0" [hidden]="!statistics?.activeUsers30d">
<h3
class="mb-0"
[hidden]="statistics?.activeUsers30d === undefined"
>
{{ statistics?.activeUsers30d ?? '-' }}
</h3>
<div class="h6 mb-0">
@ -138,7 +147,7 @@
</div>
</div>
<div class="col-xs-12 col-md-4 my-2">
<h3 class="mb-0" [hidden]="!statistics?.newUsers30d">
<h3 class="mb-0" [hidden]="statistics?.newUsers30d === undefined">
{{ statistics?.newUsers30d ?? '-' }}
</h3>
<div class="h6 mb-0">
@ -148,13 +157,19 @@
</div>
</div>
<div class="col-xs-12 col-md-4 my-2">
<h3 class="mb-0" [hidden]="!statistics?.gitHubContributors">
<h3
class="mb-0"
[hidden]="statistics?.gitHubContributors === undefined"
>
{{ statistics?.gitHubContributors ?? '-' }}
</h3>
<div class="h6 mb-0" i18n>Contributors on GitHub</div>
</div>
<div class="col-xs-12 col-md-4 my-2">
<h3 class="mb-0" [hidden]="!statistics?.gitHubStargazers">
<h3
class="mb-0"
[hidden]="statistics?.gitHubStargazers === undefined"
>
{{ statistics?.gitHubStargazers ?? '-' }}
</h3>
<div class="h6 mb-0" i18n>Stars on GitHub</div>

View File

@ -14,7 +14,6 @@
}
.intro-container {
background-color: #ffffff;
margin-top: -5rem;
.intro-inner-container {
@ -37,4 +36,10 @@
background-color: var(--dark-background);
}
}
.intro-container {
.intro {
background-image: url('/assets/intro-dark.jpg') !important;
}
}
}

View File

@ -131,7 +131,7 @@
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="unit-price w-100">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Unit Price</mat-label>
<input
matInput

View File

@ -8,7 +8,6 @@
}
.intro-container {
background-color: #ffffff;
margin-top: -5rem;
.intro-inner-container {
@ -24,3 +23,17 @@
}
}
}
:host-context(.is-dark-theme) {
.button-container {
.mat-stroked-button {
background-color: var(--dark-background);
}
}
.intro-container {
.intro {
background-image: url('/assets/intro-dark.jpg') !important;
}
}
}

View File

@ -1,5 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DataSource, MarketData } from '@prisma/client';
import { format } from 'date-fns';
@ -35,4 +37,40 @@ export class AdminService {
return this.http.post<MarketData | void>(url, {});
}
public fetchSymbolForDate({
dataSource,
date,
symbol
}: {
dataSource: DataSource;
date: Date;
symbol: string;
}) {
const url = `/api/symbol/${dataSource}/${symbol}/${format(
date,
DATE_FORMAT
)}`;
return this.http.get<IDataProviderHistoricalResponse>(url);
}
public putMarketData({
dataSource,
date,
marketData,
symbol
}: {
dataSource: DataSource;
date: Date;
marketData: UpdateMarketDataDto;
symbol: string;
}) {
const url = `/api/admin/market-data/${dataSource}/${symbol}/${format(
date,
DATE_FORMAT
)}`;
return this.http.put<MarketData>(url, marketData);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@ -1,5 +1,15 @@
version: '3.7'
services:
ghostfolio:
build: ../
env_file:
- ../.env
environment:
DATABASE_URL: postgresql://user:password@postgres:5432/ghostfolio-db?sslmode=prefer
REDIS_HOST: 'redis'
ports:
- 3333:3333
postgres:
image: postgres:12
env_file:
@ -10,15 +20,5 @@ services:
redis:
image: 'redis:alpine'
ghostfolio:
build: ../
env_file:
- ../.env
environment:
REDIS_HOST: 'redis'
DATABASE_URL: postgresql://user:password@postgres:5432/ghostfolio-db?sslmode=prefer
ports:
- 3333:3333
volumes:
postgres:

View File

@ -0,0 +1,22 @@
version: '3.7'
services:
postgres:
image: postgres:12
container_name: postgres
restart: unless-stopped
ports:
- 5432:5432
env_file:
- ../.env
volumes:
- postgres:/var/lib/postgresql/data
redis:
image: 'redis:alpine'
container_name: redis
restart: unless-stopped
ports:
- 6379:6379
volumes:
postgres:

View File

@ -1,11 +1,17 @@
version: '3.7'
services:
ghostfolio:
image: ghostfolio/ghostfolio
env_file:
- ../.env
environment:
DATABASE_URL: postgresql://user:password@postgres:5432/ghostfolio-db?sslmode=prefer
REDIS_HOST: 'redis'
ports:
- 3333:3333
postgres:
image: postgres:12
container_name: postgres
restart: unless-stopped
ports:
- 5432:5432
env_file:
- ../.env
volumes:
@ -13,10 +19,6 @@ services:
redis:
image: 'redis:alpine'
container_name: redis
restart: unless-stopped
ports:
- 6379:6379
volumes:
postgres:

View File

@ -5,6 +5,4 @@ export interface PortfolioPerformance {
currentNetPerformance: number;
currentNetPerformancePercent: number;
currentValue: number;
isAllTimeHigh: boolean;
isAllTimeLow: boolean;
}

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.89.0",
"version": "1.93.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -105,6 +105,7 @@
"passport": "0.4.1",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0",
"prisma": "3.6.0",
"reflect-metadata": "0.1.13",
"round-to": "5.0.0",
"rxjs": "7.4.0",
@ -161,7 +162,6 @@
"jest": "27.2.3",
"jest-preset-angular": "11.0.0",
"prettier": "2.3.2",
"prisma": "3.6.0",
"replace-in-file": "6.2.0",
"rimraf": "3.0.2",
"ts-jest": "27.0.5",

View File

@ -0,0 +1,59 @@
-- DropForeignKey
ALTER TABLE "Access" DROP CONSTRAINT "Access_granteeUserId_fkey";
-- DropForeignKey
ALTER TABLE "Access" DROP CONSTRAINT "Access_userId_fkey";
-- DropForeignKey
ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey";
-- DropForeignKey
ALTER TABLE "Analytics" DROP CONSTRAINT "Analytics_userId_fkey";
-- DropForeignKey
ALTER TABLE "AuthDevice" DROP CONSTRAINT "AuthDevice_userId_fkey";
-- DropForeignKey
ALTER TABLE "Order" DROP CONSTRAINT "Order_userId_fkey";
-- DropForeignKey
ALTER TABLE "Settings" DROP CONSTRAINT "Settings_userId_fkey";
-- DropForeignKey
ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_userId_fkey";
-- AddForeignKey
ALTER TABLE "Access" ADD CONSTRAINT "Access_granteeUserId_fkey" FOREIGN KEY ("granteeUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Access" ADD CONSTRAINT "Access_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Analytics" ADD CONSTRAINT "Analytics_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AuthDevice" ADD CONSTRAINT "AuthDevice_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Settings" ADD CONSTRAINT "Settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex
ALTER INDEX "MarketData.date_symbol_unique" RENAME TO "MarketData_date_symbol_key";
-- RenameIndex
ALTER INDEX "MarketData.symbol_index" RENAME TO "MarketData_symbol_idx";
-- RenameIndex
ALTER INDEX "Platform.url_unique" RENAME TO "Platform_url_key";
-- RenameIndex
ALTER INDEX "SymbolProfile.dataSource_symbol_unique" RENAME TO "SymbolProfile_dataSource_symbol_key";

View File

@ -159,12 +159,12 @@ async function main() {
{
assetClass: 'CASH',
assetSubClass: 'CRYPTOCURRENCY',
countries: null,
countries: undefined,
currency: 'USD',
dataSource: DataSource.YAHOO,
id: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e',
name: 'Bitcoin USD',
sectors: null,
sectors: undefined,
symbol: 'BTCUSD'
},
{

5
publish-docker-image.sh Executable file
View File

@ -0,0 +1,5 @@
set -xe
echo "$DOCKER_HUB_ACCESS_TOKEN" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin
docker build -t ghostfolio/ghostfolio:$TRAVIS_TAG -t ghostfolio/ghostfolio:latest .
docker push ghostfolio/ghostfolio --all-tags

View File

@ -0,0 +1,2 @@
Date,Code,Currency,Price,Quantity,Action,Fee
12/12/2021,BTC,EUR,44558.42,1,buy,0
1 Date Code Currency Price Quantity Action Fee
2 12/12/2021 BTC EUR 44558.42 1 buy 0