Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
81e83d4cea | |||
5d4156ecec | |||
4693a8baa2 | |||
773444b1e2 | |||
3c46bde8d5 | |||
63ee33b685 | |||
bc87c0a3e1 | |||
caa9fc3efa | |||
9ed82ac82b | |||
9c9ca4ab1e | |||
b0b0942162 | |||
9cbf789c22 | |||
ee5ab05d8a | |||
20731c67cb | |||
bf8856ad19 | |||
a31d79821d | |||
48ab862bb6 | |||
ba234a470e | |||
ccae660104 | |||
21ed91d184 | |||
5fd413e57e | |||
4c194c938a | |||
a4d049e53d | |||
f9c4408126 | |||
d046f1d498 | |||
ad96d6e53e | |||
747e5b63fa | |||
b1187cf880 | |||
ba9e6eab58 | |||
01feead017 | |||
6a0cfb8f77 | |||
6386786ac0 | |||
d3be6577c8 | |||
73a967a7e5 | |||
836ff6ec13 | |||
c5bb3023d3 |
11
.storybook/main.js
Normal file
11
.storybook/main.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
stories: [],
|
||||||
|
addons: ['@storybook/addon-essentials']
|
||||||
|
// uncomment the property below if you want to apply some webpack config globally
|
||||||
|
// webpackFinal: async (config, { configType }) => {
|
||||||
|
// // Make whatever fine-grained changes you need that should apply to all storybook configs
|
||||||
|
|
||||||
|
// // Return the altered config
|
||||||
|
// return config;
|
||||||
|
// },
|
||||||
|
};
|
10
.storybook/tsconfig.json
Normal file
10
.storybook/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"exclude": [
|
||||||
|
"../**/*.spec.js",
|
||||||
|
"../**/*.spec.ts",
|
||||||
|
"../**/*.spec.tsx",
|
||||||
|
"../**/*.spec.jsx"
|
||||||
|
],
|
||||||
|
"include": ["../**/*"]
|
||||||
|
}
|
82
CHANGELOG.md
82
CHANGELOG.md
@ -5,6 +5,88 @@ 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).
|
||||||
|
|
||||||
|
## 1.51.0 - 11.09.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Provided the name in the portfolio position endpoint
|
||||||
|
|
||||||
|
## 1.50.0 - 11.09.2021
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the _Fear & Greed Index_ (market mood)
|
||||||
|
- Fixed the overlap of the home button with tabs on iOS (_Add to Home Screen_)
|
||||||
|
|
||||||
|
## 1.49.0 - 08.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added labels to the allocation chart by symbol on desktop
|
||||||
|
|
||||||
|
## 1.48.0 - 07.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the attribute `precision` in the value component
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Hid the performance in the _Presenter View_
|
||||||
|
|
||||||
|
## 1.47.1 - 06.09.2021
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the search functionality for cryptocurrency symbols
|
||||||
|
|
||||||
|
## 1.46.0 - 05.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the statistics section on the about page by the _GitHub_ contributors count
|
||||||
|
- Set up _Storybook_
|
||||||
|
- Added a story for the logo component
|
||||||
|
- Added a story for the no transactions info component
|
||||||
|
- Added a story for the trend indicator component
|
||||||
|
- Added a story for the value component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Switched from gross to net performance
|
||||||
|
- Restructured the portfolio summary tab on the home page (fees and net performance)
|
||||||
|
|
||||||
|
## 1.45.0 - 04.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a link below the holdings to manage the transactions
|
||||||
|
- Added the allocation chart by symbol
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Restructured the allocations page
|
||||||
|
- Upgraded `angular` from version `12.0.4` to `12.2.4`
|
||||||
|
- Upgraded `@angular/cdk` and `@angular/material` from version `12.0.6` to `12.2.4`
|
||||||
|
- Upgraded `Nx` from version `12.5.4` to `12.8.0`
|
||||||
|
- Upgraded `prisma` from version `2.24.1` to `2.30.2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the value formatting for integers (transactions count)
|
||||||
|
|
||||||
|
## 1.44.0 - 30.08.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Extended the sub classification of assets by cash
|
||||||
|
- Upgraded `svgmap` from version `2.1.1` to `2.6.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Filtered out positions without any quantity in the positions table
|
||||||
|
- Improved the symbol lookup: allow saving with valid symbol in create or edit transaction dialog
|
||||||
|
|
||||||
## 1.43.0 - 24.08.2021
|
## 1.43.0 - 24.08.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<strong>Open Source Wealth Management Software made for Humans</strong>
|
<strong>Open Source Wealth Management Software made for Humans</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/blog/2021/07/hello-ghostfolio"><strong>Blog</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/blog/2021/07/hello-ghostfolio"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="#contributing">
|
<a href="#contributing">
|
||||||
@ -62,7 +62,7 @@ Ghostfolio is for you if you are...
|
|||||||
|
|
||||||
- ✅ Create, update and delete transactions
|
- ✅ Create, update and delete transactions
|
||||||
- ✅ Multi account management
|
- ✅ Multi account management
|
||||||
- ✅ Portfolio performance (`Today`, `YTD`, `1Y`, `5Y`, `Max`)
|
- ✅ Portfolio performance: Time-weighted rate of return (TWR) for `Today`, `YTD`, `1Y`, `5Y`, `Max`
|
||||||
- ✅ Various charts
|
- ✅ Various charts
|
||||||
- ✅ Static analysis to identify potential risks in your portfolio
|
- ✅ Static analysis to identify potential risks in your portfolio
|
||||||
- ✅ Dark Mode
|
- ✅ Dark Mode
|
||||||
@ -116,6 +116,10 @@ Please make sure you have completed the instructions from [_Setup_](#Setup).
|
|||||||
|
|
||||||
Run `yarn start:client`
|
Run `yarn start:client`
|
||||||
|
|
||||||
|
### Start _Storybook_
|
||||||
|
|
||||||
|
Run `yarn start:storybook`
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run `yarn test`
|
Run `yarn test`
|
||||||
|
89
angular.json
89
angular.json
@ -6,13 +6,16 @@
|
|||||||
"defaultProject": "api",
|
"defaultProject": "api",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@nrwl/angular:application": {
|
"@nrwl/angular:application": {
|
||||||
|
"linter": "eslint",
|
||||||
"unitTestRunner": "jest",
|
"unitTestRunner": "jest",
|
||||||
"e2eTestRunner": "cypress"
|
"e2eTestRunner": "cypress"
|
||||||
},
|
},
|
||||||
"@nrwl/angular:library": {
|
"@nrwl/angular:library": {
|
||||||
|
"linter": "eslint",
|
||||||
"unitTestRunner": "jest"
|
"unitTestRunner": "jest"
|
||||||
},
|
},
|
||||||
"@nrwl/nest": {}
|
"@nrwl/nest": {},
|
||||||
|
"@nrwl/angular:component": {}
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"api": {
|
"api": {
|
||||||
@ -239,6 +242,90 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"projectType": "library",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "libs/ui",
|
||||||
|
"sourceRoot": "libs/ui/src",
|
||||||
|
"prefix": "gf",
|
||||||
|
"architect": {
|
||||||
|
"test": {
|
||||||
|
"builder": "@nrwl/jest:jest",
|
||||||
|
"outputs": ["coverage/libs/ui"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/ui/jest.config.js",
|
||||||
|
"passWithNoTests": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/ui/src/**/*.ts", "libs/ui/src/**/*.html"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storybook": {
|
||||||
|
"builder": "@nrwl/storybook:storybook",
|
||||||
|
"options": {
|
||||||
|
"uiFramework": "@storybook/angular",
|
||||||
|
"port": 4400,
|
||||||
|
"config": {
|
||||||
|
"configFolder": "libs/ui/.storybook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"quiet": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build-storybook": {
|
||||||
|
"builder": "@nrwl/storybook:build",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"uiFramework": "@storybook/angular",
|
||||||
|
"outputPath": "dist/storybook/ui",
|
||||||
|
"config": {
|
||||||
|
"configFolder": "libs/ui/.storybook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"quiet": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ui-e2e": {
|
||||||
|
"root": "apps/ui-e2e",
|
||||||
|
"sourceRoot": "apps/ui-e2e/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"architect": {
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@nrwl/cypress:cypress",
|
||||||
|
"options": {
|
||||||
|
"cypressConfig": "apps/ui-e2e/cypress.json",
|
||||||
|
"devServerTarget": "ui:storybook",
|
||||||
|
"tsConfig": "apps/ui-e2e/tsconfig.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"devServerTarget": "ui:storybook:ci"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Access } from '@ghostfolio/common/interfaces';
|
import { Access } from '@ghostfolio/common/interfaces';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
|
@ -2,7 +2,7 @@ import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
|
|||||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
|
2
apps/api/src/app/cache/cache.controller.ts
vendored
2
apps/api/src/app/cache/cache.controller.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import { Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
import { Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { baseCurrency, benchmarks } from '@ghostfolio/common/config';
|
import { baseCurrency, benchmarks } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { isApiTokenAuthorized } from '@ghostfolio/common/permissions';
|
import { isApiTokenAuthorized } from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Export } from '@ghostfolio/common/interfaces';
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -90,6 +90,27 @@ export class InfoService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async countGitHubContributors(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const get = bent(
|
||||||
|
`https://api.github.com/repos/ghostfolio/ghostfolio/contributors`,
|
||||||
|
'GET',
|
||||||
|
'json',
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
'User-Agent': 'request'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const contributors = await get();
|
||||||
|
return contributors?.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async countGitHubStargazers(): Promise<number> {
|
private async countGitHubStargazers(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const get = bent(
|
||||||
@ -131,11 +152,13 @@ export class InfoService {
|
|||||||
|
|
||||||
const activeUsers1d = await this.countActiveUsers(1);
|
const activeUsers1d = await this.countActiveUsers(1);
|
||||||
const activeUsers30d = await this.countActiveUsers(30);
|
const activeUsers30d = await this.countActiveUsers(30);
|
||||||
|
const gitHubContributors = await this.countGitHubContributors();
|
||||||
const gitHubStargazers = await this.countGitHubStargazers();
|
const gitHubStargazers = await this.countGitHubStargazers();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeUsers1d,
|
activeUsers1d,
|
||||||
activeUsers30d,
|
activeUsers30d,
|
||||||
|
gitHubContributors,
|
||||||
gitHubStargazers
|
gitHubStargazers
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -6,6 +6,8 @@ export interface CurrentPositions {
|
|||||||
positions: TimelinePosition[];
|
positions: TimelinePosition[];
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
|
netPerformance: Big;
|
||||||
|
netPerformancePercentage: Big;
|
||||||
currentValue: Big;
|
currentValue: Big;
|
||||||
totalInvestment: Big;
|
totalInvestment: Big;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import Big from 'big.js';
|
|||||||
export interface PortfolioOrder {
|
export interface PortfolioOrder {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
date: string;
|
date: string;
|
||||||
|
fee: Big;
|
||||||
name: string;
|
name: string;
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
@ -11,6 +11,9 @@ export interface PortfolioPositionDetail {
|
|||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
maxPrice: number;
|
maxPrice: number;
|
||||||
minPrice: number;
|
minPrice: number;
|
||||||
|
name: string;
|
||||||
|
netPerformance: number;
|
||||||
|
netPerformancePercent: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
|
@ -4,5 +4,6 @@ export interface TimelinePeriod {
|
|||||||
date: string;
|
date: string;
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
|
netPerformance: Big;
|
||||||
value: Big;
|
value: Big;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import Big from 'big.js';
|
|||||||
|
|
||||||
export interface TransactionPointSymbol {
|
export interface TransactionPointSymbol {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
fee: Big;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -58,6 +58,7 @@ export class PortfolioCalculator {
|
|||||||
.plus(oldAccumulatedSymbol.quantity);
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
investment: newQuantity.eq(0)
|
investment: newQuantity.eq(0)
|
||||||
? new Big(0)
|
? new Big(0)
|
||||||
@ -72,6 +73,7 @@ export class PortfolioCalculator {
|
|||||||
} else {
|
} else {
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
fee: order.fee,
|
||||||
firstBuyDate: order.date,
|
firstBuyDate: order.date,
|
||||||
investment: unitPrice.mul(order.quantity).mul(factor),
|
investment: unitPrice.mul(order.quantity).mul(factor),
|
||||||
quantity: order.quantity.mul(factor),
|
quantity: order.quantity.mul(factor),
|
||||||
@ -112,11 +114,13 @@ export class PortfolioCalculator {
|
|||||||
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
|
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
|
||||||
if (!this.transactionPoints?.length) {
|
if (!this.transactionPoints?.length) {
|
||||||
return {
|
return {
|
||||||
|
currentValue: new Big(0),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
positions: [],
|
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
currentValue: new Big(0),
|
netPerformance: new Big(0),
|
||||||
|
netPerformancePercentage: new Big(0),
|
||||||
|
positions: [],
|
||||||
totalInvestment: new Big(0)
|
totalInvestment: new Big(0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -181,7 +185,9 @@ export class PortfolioCalculator {
|
|||||||
const startString = format(start, DATE_FORMAT);
|
const startString = format(start, DATE_FORMAT);
|
||||||
|
|
||||||
const holdingPeriodReturns: { [symbol: string]: Big } = {};
|
const holdingPeriodReturns: { [symbol: string]: Big } = {};
|
||||||
|
const netHoldingPeriodReturns: { [symbol: string]: Big } = {};
|
||||||
const grossPerformance: { [symbol: string]: Big } = {};
|
const grossPerformance: { [symbol: string]: Big } = {};
|
||||||
|
const netPerformance: { [symbol: string]: Big } = {};
|
||||||
const todayString = format(today, DATE_FORMAT);
|
const todayString = format(today, DATE_FORMAT);
|
||||||
|
|
||||||
if (firstIndex > 0) {
|
if (firstIndex > 0) {
|
||||||
@ -190,6 +196,7 @@ export class PortfolioCalculator {
|
|||||||
const invalidSymbols = [];
|
const invalidSymbols = [];
|
||||||
const lastInvestments: { [symbol: string]: Big } = {};
|
const lastInvestments: { [symbol: string]: Big } = {};
|
||||||
const lastQuantities: { [symbol: string]: Big } = {};
|
const lastQuantities: { [symbol: string]: Big } = {};
|
||||||
|
const lastFees: { [symbol: string]: Big } = {};
|
||||||
const initialValues: { [symbol: string]: Big } = {};
|
const initialValues: { [symbol: string]: Big } = {};
|
||||||
|
|
||||||
for (let i = firstIndex; i < this.transactionPoints.length; i++) {
|
for (let i = firstIndex; i < this.transactionPoints.length; i++) {
|
||||||
@ -202,10 +209,6 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
const items = this.transactionPoints[i].items;
|
const items = this.transactionPoints[i].items;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
let oldHoldingPeriodReturn = holdingPeriodReturns[item.symbol];
|
|
||||||
if (!oldHoldingPeriodReturn) {
|
|
||||||
oldHoldingPeriodReturn = new Big(1);
|
|
||||||
}
|
|
||||||
if (!marketSymbolMap[nextDate]?.[item.symbol]) {
|
if (!marketSymbolMap[nextDate]?.[item.symbol]) {
|
||||||
invalidSymbols.push(item.symbol);
|
invalidSymbols.push(item.symbol);
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
@ -224,6 +227,13 @@ export class PortfolioCalculator {
|
|||||||
const itemValue = marketSymbolMap[currentDate]?.[item.symbol];
|
const itemValue = marketSymbolMap[currentDate]?.[item.symbol];
|
||||||
let initialValue = itemValue?.mul(lastQuantity);
|
let initialValue = itemValue?.mul(lastQuantity);
|
||||||
let investedValue = itemValue?.mul(item.quantity);
|
let investedValue = itemValue?.mul(item.quantity);
|
||||||
|
const isFirstOrderAndIsStartBeforeCurrentDate =
|
||||||
|
i === firstIndex &&
|
||||||
|
isBefore(parseDate(this.transactionPoints[i].date), start);
|
||||||
|
const lastFee: Big = lastFees[item.symbol] ?? new Big(0);
|
||||||
|
const fee = isFirstOrderAndIsStartBeforeCurrentDate
|
||||||
|
? new Big(0)
|
||||||
|
: item.fee.minus(lastFee);
|
||||||
if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) {
|
if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) {
|
||||||
initialValue = item.investment;
|
initialValue = item.investment;
|
||||||
investedValue = item.investment;
|
investedValue = item.investment;
|
||||||
@ -247,18 +257,26 @@ export class PortfolioCalculator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow));
|
const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow));
|
||||||
holdingPeriodReturns[item.symbol] =
|
holdingPeriodReturns[item.symbol] = (
|
||||||
oldHoldingPeriodReturn.mul(holdingPeriodReturn);
|
holdingPeriodReturns[item.symbol] ?? new Big(1)
|
||||||
let oldGrossPerformance = grossPerformance[item.symbol];
|
).mul(holdingPeriodReturn);
|
||||||
if (!oldGrossPerformance) {
|
grossPerformance[item.symbol] = (
|
||||||
oldGrossPerformance = new Big(0);
|
grossPerformance[item.symbol] ?? new Big(0)
|
||||||
}
|
).plus(endValue.minus(investedValue));
|
||||||
const currentPerformance = endValue.minus(investedValue);
|
|
||||||
grossPerformance[item.symbol] =
|
const netHoldingPeriodReturn = endValue.div(
|
||||||
oldGrossPerformance.plus(currentPerformance);
|
initialValue.plus(cashFlow).plus(fee)
|
||||||
|
);
|
||||||
|
netHoldingPeriodReturns[item.symbol] = (
|
||||||
|
netHoldingPeriodReturns[item.symbol] ?? new Big(1)
|
||||||
|
).mul(netHoldingPeriodReturn);
|
||||||
|
netPerformance[item.symbol] = (
|
||||||
|
netPerformance[item.symbol] ?? new Big(0)
|
||||||
|
).plus(endValue.minus(investedValue).minus(fee));
|
||||||
}
|
}
|
||||||
lastInvestments[item.symbol] = item.investment;
|
lastInvestments[item.symbol] = item.investment;
|
||||||
lastQuantities[item.symbol] = item.quantity;
|
lastQuantities[item.symbol] = item.quantity;
|
||||||
|
lastFees[item.symbol] = item.fee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,15 +300,17 @@ export class PortfolioCalculator {
|
|||||||
: null,
|
: null,
|
||||||
investment: item.investment,
|
investment: item.investment,
|
||||||
marketPrice: marketValue?.toNumber() ?? null,
|
marketPrice: marketValue?.toNumber() ?? null,
|
||||||
|
netPerformance: isValid ? netPerformance[item.symbol] ?? null : null,
|
||||||
|
netPerformancePercentage:
|
||||||
|
isValid && netHoldingPeriodReturns[item.symbol]
|
||||||
|
? netHoldingPeriodReturns[item.symbol].minus(1)
|
||||||
|
: null,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
transactionCount: item.transactionCount
|
transactionCount: item.transactionCount
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const overall = this.calculateOverallGrossPerformance(
|
const overall = this.calculateOverallPerformance(positions, initialValues);
|
||||||
positions,
|
|
||||||
initialValues
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...overall,
|
...overall,
|
||||||
@ -378,7 +398,7 @@ export class PortfolioCalculator {
|
|||||||
return flatten(timelinePeriods);
|
return flatten(timelinePeriods);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateOverallGrossPerformance(
|
private calculateOverallPerformance(
|
||||||
positions: TimelinePosition[],
|
positions: TimelinePosition[],
|
||||||
initialValues: { [p: string]: Big }
|
initialValues: { [p: string]: Big }
|
||||||
) {
|
) {
|
||||||
@ -387,6 +407,8 @@ export class PortfolioCalculator {
|
|||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let grossPerformance = new Big(0);
|
let grossPerformance = new Big(0);
|
||||||
let grossPerformancePercentage = new Big(0);
|
let grossPerformancePercentage = new Big(0);
|
||||||
|
let netPerformance = new Big(0);
|
||||||
|
let netPerformancePercentage = new Big(0);
|
||||||
let completeInitialValue = new Big(0);
|
let completeInitialValue = new Big(0);
|
||||||
for (const currentPosition of positions) {
|
for (const currentPosition of positions) {
|
||||||
if (currentPosition.marketPrice) {
|
if (currentPosition.marketPrice) {
|
||||||
@ -401,6 +423,7 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance = grossPerformance.plus(
|
grossPerformance = grossPerformance.plus(
|
||||||
currentPosition.grossPerformance
|
currentPosition.grossPerformance
|
||||||
);
|
);
|
||||||
|
netPerformance = netPerformance.plus(currentPosition.netPerformance);
|
||||||
} else if (!currentPosition.quantity.eq(0)) {
|
} else if (!currentPosition.quantity.eq(0)) {
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
@ -414,6 +437,9 @@ export class PortfolioCalculator {
|
|||||||
grossPerformancePercentage = grossPerformancePercentage.plus(
|
grossPerformancePercentage = grossPerformancePercentage.plus(
|
||||||
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
||||||
);
|
);
|
||||||
|
netPerformancePercentage = netPerformancePercentage.plus(
|
||||||
|
currentPosition.netPerformancePercentage.mul(currentInitialValue)
|
||||||
|
);
|
||||||
} else if (!currentPosition.quantity.eq(0)) {
|
} else if (!currentPosition.quantity.eq(0)) {
|
||||||
console.error(
|
console.error(
|
||||||
`Initial value is missing for symbol ${currentPosition.symbol}`
|
`Initial value is missing for symbol ${currentPosition.symbol}`
|
||||||
@ -425,6 +451,8 @@ export class PortfolioCalculator {
|
|||||||
if (!completeInitialValue.eq(0)) {
|
if (!completeInitialValue.eq(0)) {
|
||||||
grossPerformancePercentage =
|
grossPerformancePercentage =
|
||||||
grossPerformancePercentage.div(completeInitialValue);
|
grossPerformancePercentage.div(completeInitialValue);
|
||||||
|
netPerformancePercentage =
|
||||||
|
netPerformancePercentage.div(completeInitialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -432,6 +460,8 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
|
netPerformance,
|
||||||
|
netPerformancePercentage,
|
||||||
totalInvestment
|
totalInvestment
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -442,6 +472,7 @@ export class PortfolioCalculator {
|
|||||||
endDate: Date
|
endDate: Date
|
||||||
): Promise<TimelinePeriod[]> {
|
): Promise<TimelinePeriod[]> {
|
||||||
let investment: Big = new Big(0);
|
let investment: Big = new Big(0);
|
||||||
|
let fees: Big = new Big(0);
|
||||||
|
|
||||||
const marketSymbolMap: {
|
const marketSymbolMap: {
|
||||||
[date: string]: { [symbol: string]: Big };
|
[date: string]: { [symbol: string]: Big };
|
||||||
@ -454,6 +485,7 @@ export class PortfolioCalculator {
|
|||||||
currencies[item.symbol] = item.currency;
|
currencies[item.symbol] = item.currency;
|
||||||
symbols.push(item.symbol);
|
symbols.push(item.symbol);
|
||||||
investment = investment.add(item.investment);
|
investment = investment.add(item.investment);
|
||||||
|
fees = fees.add(item.fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
let marketSymbols: GetValueObject[] = [];
|
let marketSymbols: GetValueObject[] = [];
|
||||||
@ -490,7 +522,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results: TimelinePeriod[] = [];
|
||||||
for (
|
for (
|
||||||
let currentDate = startDate;
|
let currentDate = startDate;
|
||||||
isBefore(currentDate, endDate);
|
isBefore(currentDate, endDate);
|
||||||
@ -513,11 +545,13 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!invalid) {
|
if (!invalid) {
|
||||||
|
const grossPerformance = value.minus(investment);
|
||||||
const result = {
|
const result = {
|
||||||
date: currentDateAsString,
|
grossPerformance,
|
||||||
grossPerformance: value.minus(investment),
|
|
||||||
investment,
|
investment,
|
||||||
value
|
value,
|
||||||
|
date: currentDateAsString,
|
||||||
|
netPerformance: grossPerformance.minus(fees)
|
||||||
};
|
};
|
||||||
results.push(result);
|
results.push(result);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
PortfolioSummary
|
PortfolioSummary
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@ -276,6 +276,7 @@ export class PortfolioController {
|
|||||||
position = nullifyValuesInObject(position, [
|
position = nullifyValuesInObject(position, [
|
||||||
'grossPerformance',
|
'grossPerformance',
|
||||||
'investment',
|
'investment',
|
||||||
|
'netPerformance',
|
||||||
'quantity'
|
'quantity'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -26,14 +26,13 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
|||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPosition,
|
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition
|
TimelinePosition
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import {
|
import type {
|
||||||
DateRange,
|
DateRange,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser
|
RequestWithUser
|
||||||
@ -148,7 +147,7 @@ export class PortfolioService {
|
|||||||
.map((timelineItem) => ({
|
.map((timelineItem) => ({
|
||||||
date: timelineItem.date,
|
date: timelineItem.date,
|
||||||
marketPrice: timelineItem.value,
|
marketPrice: timelineItem.value,
|
||||||
value: timelineItem.grossPerformance.toNumber()
|
value: timelineItem.netPerformance.toNumber()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,6 +210,11 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const item of currentPositions.positions) {
|
for (const item of currentPositions.positions) {
|
||||||
|
if (item.quantity.lte(0)) {
|
||||||
|
// Ignore positions without any quantity
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const value = item.quantity.mul(item.marketPrice);
|
const value = item.quantity.mul(item.marketPrice);
|
||||||
const symbolProfile = symbolProfileMap[item.symbol];
|
const symbolProfile = symbolProfileMap[item.symbol];
|
||||||
const dataProviderResponse = dataProviderResponses[item.symbol];
|
const dataProviderResponse = dataProviderResponses[item.symbol];
|
||||||
@ -229,6 +233,8 @@ export class PortfolioService {
|
|||||||
marketPrice: item.marketPrice,
|
marketPrice: item.marketPrice,
|
||||||
marketState: dataProviderResponse.marketState,
|
marketState: dataProviderResponse.marketState,
|
||||||
name: symbolProfile.name,
|
name: symbolProfile.name,
|
||||||
|
netPerformance: item.netPerformance?.toNumber() ?? 0,
|
||||||
|
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,
|
||||||
quantity: item.quantity.toNumber(),
|
quantity: item.quantity.toNumber(),
|
||||||
sectors: symbolProfile.sectors,
|
sectors: symbolProfile.sectors,
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
@ -276,6 +282,9 @@ export class PortfolioService {
|
|||||||
marketPrice: undefined,
|
marketPrice: undefined,
|
||||||
maxPrice: undefined,
|
maxPrice: undefined,
|
||||||
minPrice: undefined,
|
minPrice: undefined,
|
||||||
|
name: undefined,
|
||||||
|
netPerformance: undefined,
|
||||||
|
netPerformancePercent: undefined,
|
||||||
quantity: undefined,
|
quantity: undefined,
|
||||||
symbol: aSymbol,
|
symbol: aSymbol,
|
||||||
transactionCount: undefined
|
transactionCount: undefined
|
||||||
@ -283,10 +292,12 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const positionCurrency = orders[0].currency;
|
const positionCurrency = orders[0].currency;
|
||||||
|
const name = orders[0].SymbolProfile?.name ?? '';
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
|
fee: new Big(order.fee),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
@ -320,7 +331,7 @@ export class PortfolioService {
|
|||||||
transactionCount
|
transactionCount
|
||||||
} = position;
|
} = position;
|
||||||
|
|
||||||
// Convert investment and gross performance to currency of user
|
// Convert investment, gross and net performance to currency of user
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const investment = this.exchangeRateDataService.toCurrency(
|
const investment = this.exchangeRateDataService.toCurrency(
|
||||||
position.investment.toNumber(),
|
position.investment.toNumber(),
|
||||||
@ -332,6 +343,11 @@ export class PortfolioService {
|
|||||||
currency,
|
currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
|
const netPerformance = this.exchangeRateDataService.toCurrency(
|
||||||
|
position.netPerformance.toNumber(),
|
||||||
|
currency,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
[aSymbol],
|
[aSymbol],
|
||||||
@ -393,10 +409,13 @@ export class PortfolioService {
|
|||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
|
name,
|
||||||
|
netPerformance,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
averagePrice: averagePrice.toNumber(),
|
averagePrice: averagePrice.toNumber(),
|
||||||
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
||||||
historicalData: historicalDataArray,
|
historicalData: historicalDataArray,
|
||||||
|
netPerformancePercent: position.netPerformancePercentage.toNumber(),
|
||||||
quantity: quantity.toNumber(),
|
quantity: quantity.toNumber(),
|
||||||
symbol: aSymbol
|
symbol: aSymbol
|
||||||
};
|
};
|
||||||
@ -439,6 +458,7 @@ export class PortfolioService {
|
|||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
|
name,
|
||||||
averagePrice: 0,
|
averagePrice: 0,
|
||||||
currency: currentData[aSymbol]?.currency,
|
currency: currentData[aSymbol]?.currency,
|
||||||
firstBuyDate: undefined,
|
firstBuyDate: undefined,
|
||||||
@ -446,6 +466,8 @@ export class PortfolioService {
|
|||||||
grossPerformancePercent: undefined,
|
grossPerformancePercent: undefined,
|
||||||
historicalData: historicalDataArray,
|
historicalData: historicalDataArray,
|
||||||
investment: 0,
|
investment: 0,
|
||||||
|
netPerformance: undefined,
|
||||||
|
netPerformancePercent: undefined,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
symbol: aSymbol,
|
symbol: aSymbol,
|
||||||
transactionCount: undefined
|
transactionCount: undefined
|
||||||
@ -509,6 +531,9 @@ export class PortfolioService {
|
|||||||
investment: new Big(position.investment).toNumber(),
|
investment: new Big(position.investment).toNumber(),
|
||||||
marketState: dataProviderResponses[position.symbol].marketState,
|
marketState: dataProviderResponses[position.symbol].marketState,
|
||||||
name: symbolProfileMap[position.symbol].name,
|
name: symbolProfileMap[position.symbol].name,
|
||||||
|
netPerformance: position.netPerformance?.toNumber() ?? null,
|
||||||
|
netPerformancePercentage:
|
||||||
|
position.netPerformancePercentage?.toNumber() ?? null,
|
||||||
quantity: new Big(position.quantity).toNumber()
|
quantity: new Big(position.quantity).toNumber()
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -534,6 +559,8 @@ export class PortfolioService {
|
|||||||
performance: {
|
performance: {
|
||||||
currentGrossPerformance: 0,
|
currentGrossPerformance: 0,
|
||||||
currentGrossPerformancePercent: 0,
|
currentGrossPerformancePercent: 0,
|
||||||
|
currentNetPerformance: 0,
|
||||||
|
currentNetPerformancePercent: 0,
|
||||||
currentValue: 0
|
currentValue: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -553,11 +580,17 @@ export class PortfolioService {
|
|||||||
currentPositions.grossPerformance.toNumber();
|
currentPositions.grossPerformance.toNumber();
|
||||||
const currentGrossPerformancePercent =
|
const currentGrossPerformancePercent =
|
||||||
currentPositions.grossPerformancePercentage.toNumber();
|
currentPositions.grossPerformancePercentage.toNumber();
|
||||||
|
const currentNetPerformance = currentPositions.netPerformance.toNumber();
|
||||||
|
const currentNetPerformancePercent =
|
||||||
|
currentPositions.netPerformancePercentage.toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||||
performance: {
|
performance: {
|
||||||
currentGrossPerformance,
|
currentGrossPerformance,
|
||||||
currentGrossPerformancePercent,
|
currentGrossPerformancePercent,
|
||||||
|
currentNetPerformance,
|
||||||
|
currentNetPerformancePercent,
|
||||||
currentValue: currentValue
|
currentValue: currentValue
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -719,6 +752,7 @@ export class PortfolioService {
|
|||||||
allocationCurrent: cashValue.div(value).toNumber(),
|
allocationCurrent: cashValue.div(value).toNumber(),
|
||||||
allocationInvestment: cashValue.div(investment).toNumber(),
|
allocationInvestment: cashValue.div(investment).toNumber(),
|
||||||
assetClass: AssetClass.CASH,
|
assetClass: AssetClass.CASH,
|
||||||
|
assetSubClass: AssetClass.CASH,
|
||||||
countries: [],
|
countries: [],
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
grossPerformance: 0,
|
grossPerformance: 0,
|
||||||
@ -727,6 +761,8 @@ export class PortfolioService {
|
|||||||
marketPrice: 0,
|
marketPrice: 0,
|
||||||
marketState: MarketState.open,
|
marketState: MarketState.open,
|
||||||
name: 'Cash',
|
name: 'Cash',
|
||||||
|
netPerformance: 0,
|
||||||
|
netPerformancePercent: 0,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
sectors: [],
|
sectors: [],
|
||||||
symbol: ghostfolioCashSymbol,
|
symbol: ghostfolioCashSymbol,
|
||||||
@ -773,6 +809,13 @@ export class PortfolioService {
|
|||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
|
fee: new Big(
|
||||||
|
this.exchangeRateDataService.toCurrency(
|
||||||
|
order.fee,
|
||||||
|
order.currency,
|
||||||
|
userCurrency
|
||||||
|
)
|
||||||
|
),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||||
import { SymbolItem } from './interfaces/symbol-item.interface';
|
import { SymbolItem } from './interfaces/symbol-item.interface';
|
||||||
@ -48,6 +49,15 @@ export class SymbolController {
|
|||||||
@Get(':symbol')
|
@Get(':symbol')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getPosition(@Param('symbol') symbol): Promise<SymbolItem> {
|
public async getPosition(@Param('symbol') symbol): Promise<SymbolItem> {
|
||||||
return this.symbolService.get(symbol);
|
const result = await this.symbolService.get(symbol);
|
||||||
|
|
||||||
|
if (!result || isEmpty(result)) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,17 @@ export class SymbolService {
|
|||||||
|
|
||||||
public async get(aSymbol: string): Promise<SymbolItem> {
|
public async get(aSymbol: string): Promise<SymbolItem> {
|
||||||
const response = await this.dataProviderService.get([aSymbol]);
|
const response = await this.dataProviderService.get([aSymbol]);
|
||||||
const { currency, dataSource, marketPrice } = response[aSymbol];
|
const { currency, dataSource, marketPrice } = response[aSymbol] ?? {};
|
||||||
|
|
||||||
return {
|
if (dataSource && marketPrice) {
|
||||||
dataSource,
|
return {
|
||||||
marketPrice,
|
dataSource,
|
||||||
currency: <Currency>(<unknown>currency)
|
marketPrice,
|
||||||
};
|
currency: <Currency>(<unknown>currency)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { benchmarks, currencyPairs } from '@ghostfolio/common/config';
|
import {
|
||||||
|
benchmarks,
|
||||||
|
currencyPairs,
|
||||||
|
ghostfolioFearAndGreedIndexSymbol
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getUtc,
|
getUtc,
|
||||||
@ -295,7 +299,7 @@ export class DataGatheringService {
|
|||||||
benchmarksToGather.push({
|
benchmarksToGather.push({
|
||||||
dataSource: DataSource.RAKUTEN,
|
dataSource: DataSource.RAKUTEN,
|
||||||
date: startDate,
|
date: startDate,
|
||||||
symbol: 'GF.FEAR_AND_GREED_INDEX'
|
symbol: ghostfolioFearAndGreedIndexSymbol
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,10 @@ import { format } from 'date-fns';
|
|||||||
import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
|
import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
|
||||||
import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service';
|
import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
import { YahooFinanceService } from './yahoo-finance/yahoo-finance.service';
|
import {
|
||||||
|
convertToYahooFinanceSymbol,
|
||||||
|
YahooFinanceService
|
||||||
|
} from './yahoo-finance/yahoo-finance.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataProviderService {
|
export class DataProviderService {
|
||||||
@ -47,12 +50,16 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const yahooFinanceSymbols = aSymbols.filter((symbol) => {
|
const yahooFinanceSymbols = aSymbols
|
||||||
return (
|
.filter((symbol) => {
|
||||||
!isGhostfolioScraperApiSymbol(symbol) &&
|
return (
|
||||||
!isRakutenRapidApiSymbol(symbol)
|
!isGhostfolioScraperApiSymbol(symbol) &&
|
||||||
);
|
!isRakutenRapidApiSymbol(symbol)
|
||||||
});
|
);
|
||||||
|
})
|
||||||
|
.map((symbol) => {
|
||||||
|
return convertToYahooFinanceSymbol(symbol);
|
||||||
|
});
|
||||||
|
|
||||||
const response = await this.yahooFinanceService.get(yahooFinanceSymbols);
|
const response = await this.yahooFinanceService.get(yahooFinanceSymbols);
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getToday,
|
getToday,
|
||||||
@ -47,11 +48,11 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = aSymbols[0];
|
const symbol = aSymbols[0];
|
||||||
|
|
||||||
if (symbol === 'GF.FEAR_AND_GREED_INDEX') {
|
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||||
const fgi = await this.getFearAndGreedIndex();
|
const fgi = await this.getFearAndGreedIndex();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'GF.FEAR_AND_GREED_INDEX': {
|
[ghostfolioFearAndGreedIndexSymbol]: {
|
||||||
currency: undefined,
|
currency: undefined,
|
||||||
dataSource: DataSource.RAKUTEN,
|
dataSource: DataSource.RAKUTEN,
|
||||||
marketPrice: fgi.now.value,
|
marketPrice: fgi.now.value,
|
||||||
@ -82,7 +83,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = aSymbols[0];
|
const symbol = aSymbols[0];
|
||||||
|
|
||||||
if (symbol === 'GF.FEAR_AND_GREED_INDEX') {
|
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||||
const fgi = await this.getFearAndGreedIndex();
|
const fgi = await this.getFearAndGreedIndex();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -118,7 +119,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'GF.FEAR_AND_GREED_INDEX': {
|
[ghostfolioFearAndGreedIndexSymbol]: {
|
||||||
[format(getYesterday(), DATE_FORMAT)]: {
|
[format(getYesterday(), DATE_FORMAT)]: {
|
||||||
marketPrice: fgi.previousClose.value
|
marketPrice: fgi.previousClose.value
|
||||||
}
|
}
|
||||||
|
@ -43,16 +43,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async get(
|
||||||
aSymbols: string[]
|
aYahooFinanceSymbols: string[]
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
if (aSymbols.length <= 0) {
|
if (aYahooFinanceSymbols.length <= 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const yahooSymbols = aSymbols.map((symbol) => {
|
|
||||||
return this.convertToYahooSymbol(symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
|
|
||||||
@ -60,12 +56,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
[symbol: string]: IYahooFinanceQuoteResponse;
|
[symbol: string]: IYahooFinanceQuoteResponse;
|
||||||
} = await yahooFinance.quote({
|
} = await yahooFinance.quote({
|
||||||
modules: ['price', 'summaryProfile'],
|
modules: ['price', 'summaryProfile'],
|
||||||
symbols: yahooSymbols
|
symbols: aYahooFinanceSymbols
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [yahooSymbol, value] of Object.entries(data)) {
|
for (const [yahooFinanceSymbol, value] of Object.entries(data)) {
|
||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
const symbol = convertFromYahooSymbol(yahooSymbol);
|
const symbol = convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
||||||
|
|
||||||
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
|
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
|
||||||
|
|
||||||
@ -136,15 +132,15 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const yahooSymbols = aSymbols.map((symbol) => {
|
const yahooFinanceSymbols = aSymbols.map((symbol) => {
|
||||||
return this.convertToYahooSymbol(symbol);
|
return convertToYahooFinanceSymbol(symbol);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historicalData: {
|
const historicalData: {
|
||||||
[symbol: string]: IYahooFinanceHistoricalResponse[];
|
[symbol: string]: IYahooFinanceHistoricalResponse[];
|
||||||
} = await yahooFinance.historical({
|
} = await yahooFinance.historical({
|
||||||
symbols: yahooSymbols,
|
symbols: yahooFinanceSymbols,
|
||||||
from: format(from, DATE_FORMAT),
|
from: format(from, DATE_FORMAT),
|
||||||
to: format(to, DATE_FORMAT)
|
to: format(to, DATE_FORMAT)
|
||||||
});
|
});
|
||||||
@ -153,9 +149,11 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
for (const [yahooSymbol, timeSeries] of Object.entries(historicalData)) {
|
for (const [yahooFinanceSymbol, timeSeries] of Object.entries(
|
||||||
|
historicalData
|
||||||
|
)) {
|
||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
const symbol = convertFromYahooSymbol(yahooSymbol);
|
const symbol = convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
||||||
response[symbol] = {};
|
response[symbol] = {};
|
||||||
|
|
||||||
timeSeries.forEach((timeSerie) => {
|
timeSeries.forEach((timeSerie) => {
|
||||||
@ -175,7 +173,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||||
let items: LookupItem[] = [];
|
const items: LookupItem[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const get = bent(
|
||||||
@ -192,19 +190,6 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
// filter out undefined symbols
|
// filter out undefined symbols
|
||||||
return quote.symbol;
|
return quote.symbol;
|
||||||
})
|
})
|
||||||
.filter(({ quoteType }) => {
|
|
||||||
return quoteType === 'EQUITY' || quoteType === 'ETF';
|
|
||||||
})
|
|
||||||
.map(({ symbol }) => {
|
|
||||||
return symbol;
|
|
||||||
});
|
|
||||||
|
|
||||||
const marketData = await this.get(symbols);
|
|
||||||
|
|
||||||
items = searchResult.quotes
|
|
||||||
.filter((quote) => {
|
|
||||||
return quote.isYahooFinance;
|
|
||||||
})
|
|
||||||
.filter(({ quoteType }) => {
|
.filter(({ quoteType }) => {
|
||||||
return (
|
return (
|
||||||
quoteType === 'CRYPTOCURRENCY' ||
|
quoteType === 'CRYPTOCURRENCY' ||
|
||||||
@ -220,42 +205,25 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map(({ longname, shortname, symbol }) => {
|
.map(({ symbol }) => {
|
||||||
return {
|
return symbol;
|
||||||
currency: marketData[symbol]?.currency,
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
name: longname || shortname,
|
|
||||||
symbol: convertFromYahooSymbol(symbol)
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const marketData = await this.get(symbols);
|
||||||
|
|
||||||
|
for (const [symbol, value] of Object.entries(marketData)) {
|
||||||
|
items.push({
|
||||||
|
symbol,
|
||||||
|
currency: value.currency,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
|
name: value.name
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return { items };
|
return { items };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a symbol to a Yahoo symbol
|
|
||||||
*
|
|
||||||
* Currency: USDCHF=X
|
|
||||||
* Cryptocurrency: BTC-USD
|
|
||||||
*/
|
|
||||||
private convertToYahooSymbol(aSymbol: string) {
|
|
||||||
if (isCurrency(aSymbol)) {
|
|
||||||
if (isCrypto(aSymbol)) {
|
|
||||||
// Add a dash before the last three characters
|
|
||||||
// BTCUSD -> BTC-USD
|
|
||||||
// DOGEUSD -> DOGE-USD
|
|
||||||
return `${aSymbol.substring(0, aSymbol.length - 3)}-${aSymbol.substring(
|
|
||||||
aSymbol.length - 3
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${aSymbol}=X`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return aSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseAssetClass(aPrice: IYahooFinancePrice): {
|
private parseAssetClass(aPrice: IYahooFinancePrice): {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
||||||
@ -290,7 +258,30 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertFromYahooSymbol = (aSymbol: string) => {
|
export const convertFromYahooFinanceSymbol = (aYahooFinanceSymbol: string) => {
|
||||||
const symbol = aSymbol.replace('-', '');
|
const symbol = aYahooFinanceSymbol.replace('-', '');
|
||||||
return symbol.replace('=X', '');
|
return symbol.replace('=X', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a symbol to a Yahoo Finance symbol
|
||||||
|
*
|
||||||
|
* Currency: USDCHF=X
|
||||||
|
* Cryptocurrency: BTC-USD
|
||||||
|
*/
|
||||||
|
export const convertToYahooFinanceSymbol = (aSymbol: string) => {
|
||||||
|
if (isCurrency(aSymbol)) {
|
||||||
|
if (isCrypto(aSymbol)) {
|
||||||
|
// Add a dash before the last three characters
|
||||||
|
// BTCUSD -> BTC-USD
|
||||||
|
// DOGEUSD -> DOGE-USD
|
||||||
|
return `${aSymbol.substring(0, aSymbol.length - 3)}-${aSymbol.substring(
|
||||||
|
aSymbol.length - 3
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${aSymbol}=X`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return aSymbol;
|
||||||
|
};
|
||||||
|
@ -5,10 +5,10 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { AccountsTableComponent } from './accounts-table.component';
|
import { AccountsTableComponent } from './accounts-table.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -5,7 +5,7 @@ import { MatMenuModule } from '@angular/material/menu';
|
|||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||||
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
|
|
||||||
import { HeaderComponent } from './header.component';
|
import { HeaderComponent } from './header.component';
|
||||||
|
|
||||||
|
@ -3,12 +3,12 @@ import { NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
||||||
import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module';
|
import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module';
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PerformanceChartDialog } from './performance-chart-dialog.component';
|
import { PerformanceChartDialog } from './performance-chart-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
|
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@ -46,7 +46,7 @@
|
|||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="
|
[value]="
|
||||||
isLoading ? undefined : performance?.currentGrossPerformancePercent
|
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||||
"
|
"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,7 +52,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
|
|
||||||
new CountUp(
|
new CountUp(
|
||||||
'value',
|
'value',
|
||||||
this.performance?.currentGrossPerformancePercent * 100,
|
this.performance?.currentNetPerformancePercent * 100,
|
||||||
{
|
{
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
duration: 0.75,
|
duration: 0.75,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
|
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -16,6 +16,7 @@ import { LinearScale } from 'chart.js';
|
|||||||
import { ArcElement } from 'chart.js';
|
import { ArcElement } from 'chart.js';
|
||||||
import { DoughnutController } from 'chart.js';
|
import { DoughnutController } from 'chart.js';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||||
import * as Color from 'color';
|
import * as Color from 'color';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -32,6 +33,7 @@ export class PortfolioProportionChartComponent
|
|||||||
@Input() keys: string[];
|
@Input() keys: string[];
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() maxItems?: number;
|
@Input() maxItems?: number;
|
||||||
|
@Input() showLabels = false;
|
||||||
@Input() positions: {
|
@Input() positions: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'type'> & { value: number };
|
[symbol: string]: Pick<PortfolioPosition, 'type'> & { value: number };
|
||||||
};
|
};
|
||||||
@ -48,7 +50,13 @@ export class PortfolioProportionChartComponent
|
|||||||
};
|
};
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip);
|
Chart.register(
|
||||||
|
ArcElement,
|
||||||
|
ChartDataLabels,
|
||||||
|
DoughnutController,
|
||||||
|
LinearScale,
|
||||||
|
Tooltip
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
@ -167,7 +175,8 @@ export class PortfolioProportionChartComponent
|
|||||||
// Reuse color
|
// Reuse color
|
||||||
item.color = this.colorMap[symbol];
|
item.color = this.colorMap[symbol];
|
||||||
} else {
|
} else {
|
||||||
const color = this.getColorPalette()[index];
|
const color =
|
||||||
|
this.getColorPalette()[index % this.getColorPalette().length];
|
||||||
|
|
||||||
// Store color for reuse
|
// Store color for reuse
|
||||||
this.colorMap[symbol] = color;
|
this.colorMap[symbol] = color;
|
||||||
@ -234,7 +243,30 @@ export class PortfolioProportionChartComponent
|
|||||||
data,
|
data,
|
||||||
options: {
|
options: {
|
||||||
cutout: '70%',
|
cutout: '70%',
|
||||||
|
layout: {
|
||||||
|
padding: this.showLabels === true ? 100 : 0
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
color: (context) => {
|
||||||
|
return this.getColorPalette()[
|
||||||
|
context.dataIndex % this.getColorPalette().length
|
||||||
|
];
|
||||||
|
},
|
||||||
|
display: this.showLabels === true ? 'auto' : false,
|
||||||
|
labels: {
|
||||||
|
index: {
|
||||||
|
align: 'end',
|
||||||
|
anchor: 'end',
|
||||||
|
formatter: (value, context) => {
|
||||||
|
return value > 0
|
||||||
|
? context.chart.data.labels[context.dataIndex]
|
||||||
|
: '';
|
||||||
|
},
|
||||||
|
offset: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
@ -9,23 +9,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3">
|
|
||||||
<div class="d-flex flex-grow-1" i18n>
|
|
||||||
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
|
|
||||||
{order} other {orders}}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-value
|
|
||||||
class="justify-content-end"
|
|
||||||
[currency]="baseCurrency"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="isLoading ? undefined : summary?.fees"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><hr /></div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
@ -66,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Absolute Performance</div>
|
<div class="d-flex flex-grow-1" i18n>Absolute Gross Performance</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
@ -77,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1 ml-3" i18n>Performance (TWR)</div>
|
<div class="d-flex flex-grow-1 ml-3" i18n>Gross Performance (TWR)</div>
|
||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
@ -91,6 +74,48 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>
|
||||||
|
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
|
||||||
|
{order} other {orders}}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.fees"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Absolute Net Performance</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.currentNetPerformance"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1 ml-3" i18n>Net Performance (TWR)</div>
|
||||||
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -3,5 +3,4 @@ export interface PositionDetailDialogParams {
|
|||||||
deviceType: string;
|
deviceType: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
title: string;
|
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,11 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
public marketPrice: number;
|
public marketPrice: number;
|
||||||
public maxPrice: number;
|
public maxPrice: number;
|
||||||
public minPrice: number;
|
public minPrice: number;
|
||||||
|
public name: string;
|
||||||
|
public netPerformance: number;
|
||||||
|
public netPerformancePercent: number;
|
||||||
public quantity: number;
|
public quantity: number;
|
||||||
|
public symbol: string;
|
||||||
public transactionCount: number;
|
public transactionCount: number;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -60,7 +64,11 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
|
name,
|
||||||
|
netPerformance,
|
||||||
|
netPerformancePercent,
|
||||||
quantity,
|
quantity,
|
||||||
|
symbol,
|
||||||
transactionCount
|
transactionCount
|
||||||
}) => {
|
}) => {
|
||||||
this.averagePrice = averagePrice;
|
this.averagePrice = averagePrice;
|
||||||
@ -86,7 +94,11 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
this.marketPrice = marketPrice;
|
this.marketPrice = marketPrice;
|
||||||
this.maxPrice = maxPrice;
|
this.maxPrice = maxPrice;
|
||||||
this.minPrice = minPrice;
|
this.minPrice = minPrice;
|
||||||
|
this.name = name;
|
||||||
|
this.netPerformance = netPerformance;
|
||||||
|
this.netPerformancePercent = netPerformancePercent;
|
||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
|
this.symbol = symbol;
|
||||||
this.transactionCount = transactionCount;
|
this.transactionCount = transactionCount;
|
||||||
|
|
||||||
if (isToday(parseISO(this.firstBuyDate))) {
|
if (isToday(parseISO(this.firstBuyDate))) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<gf-dialog-header
|
<gf-dialog-header
|
||||||
mat-dialog-title
|
mat-dialog-title
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[title]="data.title ?? data.symbol"
|
[title]="name ?? symbol"
|
||||||
(closeButtonClicked)="onClose()"
|
(closeButtonClicked)="onClose()"
|
||||||
></gf-dialog-header>
|
></gf-dialog-header>
|
||||||
|
|
||||||
@ -25,7 +25,7 @@
|
|||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[currency]="data.baseCurrency"
|
[currency]="data.baseCurrency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="grossPerformance"
|
[value]="netPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
@ -35,7 +35,7 @@
|
|||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="grossPerformancePercent"
|
[value]="netPerformancePercent"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
@ -80,7 +80,8 @@
|
|||||||
<gf-value
|
<gf-value
|
||||||
label="Quantity"
|
label="Quantity"
|
||||||
size="medium"
|
size="medium"
|
||||||
[isCurrency]="true"
|
[locale]="data.locale"
|
||||||
|
[precision]="2"
|
||||||
[value]="quantity"
|
[value]="quantity"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
@ -102,9 +103,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Transactions"
|
|
||||||
size="medium"
|
size="medium"
|
||||||
[isCurrency]="true"
|
[label]="transactionCount === 1 ? 'Transaction' : 'Transactions'"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="transactionCount"
|
[value]="transactionCount"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
|
@ -3,11 +3,11 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfDialogFooterModule } from '../../dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '../../dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '../../dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '../../dialog-header/dialog-header.module';
|
||||||
import { GfValueModule } from '../../value/value.module';
|
|
||||||
import { PositionDetailDialog } from './position-detail-dialog.component';
|
import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
[isLoading]="isLoading"
|
[isLoading]="isLoading"
|
||||||
[marketState]="position?.marketState"
|
[marketState]="position?.marketState"
|
||||||
[range]="range"
|
[range]="range"
|
||||||
[value]="position?.grossPerformancePercentage"
|
[value]="position?.netPerformancePercentage"
|
||||||
></gf-trend-indicator>
|
></gf-trend-indicator>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isLoading" class="flex-grow-1">
|
<div *ngIf="isLoading" class="flex-grow-1">
|
||||||
@ -47,13 +47,13 @@
|
|||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[currency]="baseCurrency"
|
[currency]="baseCurrency"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="position?.grossPerformance"
|
[value]="position?.netPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
<gf-value
|
<gf-value
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="position?.grossPerformancePercentage"
|
[value]="position?.netPerformancePercentage"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,8 +64,7 @@ export class PositionComponent implements OnDestroy, OnInit {
|
|||||||
baseCurrency: this.baseCurrency,
|
baseCurrency: this.baseCurrency,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
locale: this.locale,
|
locale: this.locale,
|
||||||
symbol: this.position?.symbol,
|
symbol: this.position?.symbol
|
||||||
title: this.position?.name
|
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
@ -3,10 +3,10 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfTrendIndicatorModule } from '../trend-indicator/trend-indicator.module';
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module';
|
import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module';
|
||||||
import { PositionComponent } from './position.component';
|
import { PositionComponent } from './position.component';
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.grossPerformancePercent"
|
[value]="isLoading ? undefined : element.netPerformancePercent"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -87,7 +87,7 @@
|
|||||||
}"
|
}"
|
||||||
(click)="
|
(click)="
|
||||||
!this.ignoreAssetClasses.includes(row.assetClass) &&
|
!this.ignoreAssetClasses.includes(row.assetClass) &&
|
||||||
onOpenPositionDialog({ symbol: row.symbol, title: row.name })
|
onOpenPositionDialog({ symbol: row.symbol })
|
||||||
"
|
"
|
||||||
></tr>
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -57,14 +57,9 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
this.routeQueryParams = route.queryParams
|
this.routeQueryParams = route.queryParams
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (
|
if (params['positionDetailDialog'] && params['symbol']) {
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol'] &&
|
|
||||||
params['title']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
this.openPositionDialog({
|
||||||
symbol: params['symbol'],
|
symbol: params['symbol']
|
||||||
title: params['title']
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -96,15 +91,9 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
public onOpenPositionDialog({
|
public onOpenPositionDialog({ symbol }: { symbol: string }): void {
|
||||||
symbol,
|
|
||||||
title
|
|
||||||
}: {
|
|
||||||
symbol: string;
|
|
||||||
title: string;
|
|
||||||
}): void {
|
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { positionDetailDialog: true, symbol, title }
|
queryParams: { positionDetailDialog: true, symbol }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,18 +105,11 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public openPositionDialog({
|
public openPositionDialog({ symbol }: { symbol: string }): void {
|
||||||
symbol,
|
|
||||||
title
|
|
||||||
}: {
|
|
||||||
symbol: string;
|
|
||||||
title: string;
|
|
||||||
}): void {
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
title,
|
|
||||||
baseCurrency: this.baseCurrency,
|
baseCurrency: this.baseCurrency,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
locale: this.locale
|
locale: this.locale
|
||||||
|
@ -8,12 +8,12 @@ import { MatSortModule } from '@angular/material/sort';
|
|||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfNoTransactionsInfoModule } from '../no-transactions-info/no-transactions-info.module';
|
|
||||||
import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module';
|
import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module';
|
||||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { PositionsTableComponent } from './positions-table.component';
|
import { PositionsTableComponent } from './positions-table.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
import { GfNoTransactionsInfoModule } from '../no-transactions-info/no-transactions-info.module';
|
|
||||||
import { GfPositionModule } from '../position/position.module';
|
import { GfPositionModule } from '../position/position.module';
|
||||||
import { PositionsComponent } from './positions.component';
|
import { PositionsComponent } from './positions.component';
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
||||||
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
import { GfNoTransactionsInfoModule } from '../no-transactions-info/no-transactions-info.module';
|
|
||||||
import { GfPositionModule } from '../position/position.module';
|
import { GfPositionModule } from '../position/position.module';
|
||||||
import { RulesComponent } from './rules.component';
|
import { RulesComponent } from './rules.component';
|
||||||
|
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
<mat-radio-group [formControl]="option" (change)="onValueChange()">
|
<mat-radio-group
|
||||||
|
class="text-nowrap"
|
||||||
|
[formControl]="option"
|
||||||
|
(change)="onValueChange()"
|
||||||
|
>
|
||||||
<mat-radio-button
|
<mat-radio-button
|
||||||
*ngFor="let option of options"
|
*ngFor="let option of options"
|
||||||
[disabled]="isLoading"
|
[disabled]="isLoading"
|
||||||
|
@ -255,8 +255,7 @@
|
|||||||
mat-row
|
mat-row
|
||||||
(click)="
|
(click)="
|
||||||
onOpenPositionDialog({
|
onOpenPositionDialog({
|
||||||
symbol: row.symbol,
|
symbol: row.symbol
|
||||||
title: row.SymbolProfile?.name
|
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
></tr>
|
></tr>
|
||||||
|
@ -86,8 +86,7 @@ export class TransactionsTableComponent
|
|||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (params['positionDetailDialog'] && params['symbol']) {
|
if (params['positionDetailDialog'] && params['symbol']) {
|
||||||
this.openPositionDialog({
|
this.openPositionDialog({
|
||||||
symbol: params['symbol'],
|
symbol: params['symbol']
|
||||||
title: params['title']
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -196,15 +195,9 @@ export class TransactionsTableComponent
|
|||||||
this.import.emit();
|
this.import.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onOpenPositionDialog({
|
public onOpenPositionDialog({ symbol }: { symbol: string }): void {
|
||||||
symbol,
|
|
||||||
title
|
|
||||||
}: {
|
|
||||||
symbol: string;
|
|
||||||
title: string;
|
|
||||||
}): void {
|
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { positionDetailDialog: true, symbol, title }
|
queryParams: { positionDetailDialog: true, symbol }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,18 +209,11 @@ export class TransactionsTableComponent
|
|||||||
this.transactionToClone.emit(aTransaction);
|
this.transactionToClone.emit(aTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openPositionDialog({
|
public openPositionDialog({ symbol }: { symbol: string }): void {
|
||||||
symbol,
|
|
||||||
title
|
|
||||||
}: {
|
|
||||||
symbol: string;
|
|
||||||
title: string;
|
|
||||||
}): void {
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
title,
|
|
||||||
baseCurrency: this.baseCurrency,
|
baseCurrency: this.baseCurrency,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
locale: this.locale
|
locale: this.locale
|
||||||
|
@ -10,11 +10,11 @@ import { MatSortModule } from '@angular/material/sort';
|
|||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module';
|
import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module';
|
||||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||||
import { GfValueModule } from '../value/value.module';
|
|
||||||
import { TransactionsTableComponent } from './transactions-table.component';
|
import { TransactionsTableComponent } from './transactions-table.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -94,27 +94,37 @@
|
|||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<h3 class="mb-0" [hidden]="!statistics?.activeUsers1d">
|
<h3 class="mb-0" [hidden]="!statistics?.activeUsers1d">
|
||||||
{{ statistics?.activeUsers1d ?? '-' }}
|
{{ statistics?.activeUsers1d ?? '-' }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="h6 mb-0">
|
<div class="h6 mb-0">
|
||||||
Active Users <small class="text-muted">(Last 24 hours)</small>
|
<span i18n>Active Users</span> <small class="text-muted"
|
||||||
|
>(Last 24 hours)</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<h3 class="mb-0" [hidden]="!statistics?.activeUsers30d">
|
<h3 class="mb-0" [hidden]="!statistics?.activeUsers30d">
|
||||||
{{ statistics?.activeUsers30d ?? '-' }}
|
{{ statistics?.activeUsers30d ?? '-' }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="h6 mb-0">
|
<div class="h6 mb-0">
|
||||||
Active Users <small class="text-muted">(Last 30 days)</small>
|
<span i18n>Active Users</span> <small class="text-muted"
|
||||||
|
>(Last 30 days)</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
|
<h3 class="mb-0" [hidden]="!statistics?.gitHubContributors">
|
||||||
|
{{ statistics?.gitHubContributors ?? '-' }}
|
||||||
|
</h3>
|
||||||
|
<div class="h6 mb-0" i18n>Contributors on GitHub</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-3 my-2">
|
||||||
<h3 class="mb-0" [hidden]="!statistics?.gitHubStargazers">
|
<h3 class="mb-0" [hidden]="!statistics?.gitHubStargazers">
|
||||||
{{ statistics?.gitHubStargazers ?? '-' }}
|
{{ statistics?.gitHubStargazers ?? '-' }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="h6 mb-0">Stars on GitHub</div>
|
<div class="h6 mb-0" i18n>Stars on GitHub</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
SettingsStorageService
|
SettingsStorageService
|
||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
@ -58,6 +59,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
public fearAndGreedIndex: number;
|
public fearAndGreedIndex: number;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public hasPositions: boolean;
|
public hasPositions: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
@ -110,7 +112,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchSymbolItem('GF.FEAR_AND_GREED_INDEX')
|
.fetchSymbolItem(ghostfolioFearAndGreedIndexSymbol)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketPrice }) => {
|
.subscribe(({ marketPrice }) => {
|
||||||
this.fearAndGreedIndex = marketPrice;
|
this.fearAndGreedIndex = marketPrice;
|
||||||
@ -119,6 +121,11 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.hasPermissionToCreateOrder = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.createOrder
|
||||||
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -135,6 +142,8 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dateRange =
|
this.dateRange =
|
||||||
|
@ -91,18 +91,28 @@
|
|||||||
(change)="onChangeDateRange($event.value)"
|
(change)="onChangeDateRange($event.value)"
|
||||||
></gf-toggle>
|
></gf-toggle>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-container *ngIf="hasPositions === true">
|
||||||
<mat-card *ngIf="hasPositions === true" class="p-0">
|
<mat-card class="p-0">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-positions
|
<gf-positions
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="positions"
|
[positions]="positions"
|
||||||
[range]="dateRange"
|
[range]="dateRange"
|
||||||
></gf-positions>
|
></gf-positions>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||||
|
<a
|
||||||
|
class="mt-3"
|
||||||
|
i18n
|
||||||
|
mat-button
|
||||||
|
[routerLink]="['/portfolio', 'transactions']"
|
||||||
|
>Manage Transactions...</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
<div
|
<div
|
||||||
*ngIf="hasPositions === false"
|
*ngIf="hasPositions === false"
|
||||||
class="d-flex justify-content-center"
|
class="d-flex justify-content-center"
|
||||||
|
@ -5,12 +5,12 @@ import { MatCardModule } from '@angular/material/card';
|
|||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/client/components/no-transactions-info/no-transactions-info.module';
|
|
||||||
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
|
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
|
||||||
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||||
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
import { HomePageRoutingModule } from './home-page-routing.module';
|
import { HomePageRoutingModule } from './home-page-routing.module';
|
||||||
import { HomePageComponent } from './home-page.component';
|
import { HomePageComponent } from './home-page.component';
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
|
margin-bottom: env(safe-area-inset-bottom);
|
||||||
|
margin-bottom: constant(safe-area-inset-bottom);
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-tab-body-wrapper {
|
.mat-tab-body-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -3,7 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
|
|
||||||
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
||||||
import { LandingPageComponent } from './landing-page.component';
|
import { LandingPageComponent } from './landing-page.component';
|
||||||
|
@ -42,6 +42,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
public sectors: {
|
public sectors: {
|
||||||
[name: string]: { name: string; value: number };
|
[name: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
|
public symbols: {
|
||||||
|
[name: string]: { name: string; value: number };
|
||||||
|
};
|
||||||
|
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -114,6 +118,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
value: 0
|
value: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.symbols = {
|
||||||
|
[UNKNOWN_KEY]: {
|
||||||
|
name: UNKNOWN_KEY,
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (const [name, { current, original }] of Object.entries(
|
for (const [name, { current, original }] of Object.entries(
|
||||||
this.portfolioDetails.accounts
|
this.portfolioDetails.accounts
|
||||||
@ -208,6 +218,13 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
: this.portfolioDetails.holdings[symbol].value;
|
: this.portfolioDetails.holdings[symbol].value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (position.assetClass === AssetClass.EQUITY) {
|
||||||
|
this.symbols[symbol] = {
|
||||||
|
name: symbol,
|
||||||
|
value: aPeriod === 'original' ? position.investment : position.value
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="proportion-charts row">
|
<div class="proportion-charts row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title i18n>By Account</mat-card-title>
|
<mat-card-title class="text-truncate" i18n>By Account</mat-card-title>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -27,10 +27,12 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title i18n>By Asset Class</mat-card-title>
|
<mat-card-title class="text-truncate" i18n
|
||||||
|
>By Asset Class</mat-card-title
|
||||||
|
>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -49,10 +51,12 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title i18n>By Currency</mat-card-title>
|
<mat-card-title class="text-truncate" i18n
|
||||||
|
>By Currency</mat-card-title
|
||||||
|
>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -71,10 +75,34 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-12 allocations-by-symbol">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title i18n>By Sector</mat-card-title>
|
<mat-card-title class="text-truncate" i18n>By Symbol</mat-card-title>
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="period"
|
||||||
|
[isLoading]="false"
|
||||||
|
[options]="periodOptions"
|
||||||
|
(change)="onChangePeriod($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-portfolio-proportion-chart
|
||||||
|
class="mx-auto"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[isInPercent]="false"
|
||||||
|
[keys]="['name']"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[positions]="symbols"
|
||||||
|
[showLabels]="deviceType !== 'mobile'"
|
||||||
|
></gf-portfolio-proportion-chart>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
|
<mat-card-title class="text-truncate" i18n>By Sector</mat-card-title>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -94,10 +122,12 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title i18n>By Continent</mat-card-title>
|
<mat-card-title class="text-truncate" i18n
|
||||||
|
>By Continent</mat-card-title
|
||||||
|
>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -116,10 +146,10 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title i18n>By Country</mat-card-title>
|
<mat-card-title class="text-truncate" i18n>By Country</mat-card-title>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -143,8 +173,8 @@
|
|||||||
<div class="row world-map-chart">
|
<div class="row world-map-chart">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title i18n>Regions</mat-card-title>
|
<mat-card-title class="text-truncate" i18n>Regions</mat-card-title>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
.proportion-charts {
|
.allocations-by-symbol {
|
||||||
.mat-card {
|
gf-portfolio-proportion-chart {
|
||||||
.mat-card-content {
|
max-width: 80vh;
|
||||||
padding: 1rem 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,8 +11,9 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { EMPTY, Observable, Subject } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
catchError,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
startWith,
|
startWith,
|
||||||
@ -49,7 +50,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
public ngOnInit() {
|
||||||
const { currencies, platforms } = this.dataService.fetchInfo();
|
const { currencies, platforms } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
this.currencies = currencies;
|
this.currencies = currencies;
|
||||||
@ -84,17 +85,45 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
this.data.transaction.unitPrice = this.currentMarketPrice;
|
this.data.transaction.unitPrice = this.currentMarketPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onBlurSymbol() {
|
||||||
|
const symbol = this.searchSymbolCtrl.value;
|
||||||
|
this.updateSymbol(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
public onCancel(): void {
|
public onCancel(): void {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
||||||
|
this.updateSymbol(event.option.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSymbol(symbol: string) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.data.transaction.symbol = event.option.value;
|
|
||||||
|
this.data.transaction.symbol = symbol;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchSymbolItem(this.data.transaction.symbol)
|
.fetchSymbolItem(this.data.transaction.symbol)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(
|
||||||
|
catchError(() => {
|
||||||
|
this.data.transaction.currency = null;
|
||||||
|
this.data.transaction.dataSource = null;
|
||||||
|
this.data.transaction.unitPrice = null;
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
takeUntil(this.unsubscribeSubject)
|
||||||
|
)
|
||||||
.subscribe(({ currency, dataSource, marketPrice }) => {
|
.subscribe(({ currency, dataSource, marketPrice }) => {
|
||||||
this.data.transaction.currency = currency;
|
this.data.transaction.currency = currency;
|
||||||
this.data.transaction.dataSource = dataSource;
|
this.data.transaction.dataSource = dataSource;
|
||||||
@ -105,17 +134,4 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateSymbolByTyping(value: string) {
|
|
||||||
this.data.transaction.currency = null;
|
|
||||||
this.data.transaction.dataSource = null;
|
|
||||||
this.data.transaction.unitPrice = null;
|
|
||||||
|
|
||||||
this.data.transaction.symbol = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.unsubscribeSubject.next();
|
|
||||||
this.unsubscribeSubject.complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
required
|
required
|
||||||
[formControl]="searchSymbolCtrl"
|
[formControl]="searchSymbolCtrl"
|
||||||
[matAutocomplete]="auto"
|
[matAutocomplete]="auto"
|
||||||
(change)="onUpdateSymbolByTyping($event.target.value)"
|
(blur)="onBlurSymbol()"
|
||||||
/>
|
/>
|
||||||
<mat-autocomplete
|
<mat-autocomplete
|
||||||
#auto="matAutocomplete"
|
#auto="matAutocomplete"
|
||||||
|
@ -9,8 +9,8 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { GfValueModule } from '@ghostfolio/client/components/value/value.module';
|
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
|
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
|
|
||||||
import { RegisterPageRoutingModule } from './register-page-routing.module';
|
import { RegisterPageRoutingModule } from './register-page-routing.module';
|
||||||
import { RegisterPageComponent } from './register-page.component';
|
import { RegisterPageComponent } from './register-page.component';
|
||||||
|
@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
|
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
|
||||||
|
|
||||||
import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
|
import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
Position,
|
Position,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@ -36,6 +37,7 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
public dateRange: DateRange = 'max';
|
public dateRange: DateRange = 'max';
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public hasPositions: boolean;
|
public hasPositions: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
@ -63,6 +65,11 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToCreateOrder = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.createOrder
|
||||||
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -76,6 +83,8 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
|
@ -64,17 +64,28 @@
|
|||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="align-items-center col">
|
<div class="align-items-center col">
|
||||||
<mat-card *ngIf="hasPositions === true" class="p-0">
|
<ng-container *ngIf="hasPositions === true">
|
||||||
<mat-card-content>
|
<mat-card class="p-0">
|
||||||
<gf-positions
|
<mat-card-content>
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
<gf-positions
|
||||||
[deviceType]="deviceType"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[locale]="user?.settings?.locale"
|
[deviceType]="deviceType"
|
||||||
[positions]="positions"
|
[locale]="user?.settings?.locale"
|
||||||
[range]="dateRange"
|
[positions]="positions"
|
||||||
></gf-positions>
|
[range]="dateRange"
|
||||||
</mat-card-content>
|
></gf-positions>
|
||||||
</mat-card>
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||||
|
<a
|
||||||
|
class="mt-3"
|
||||||
|
i18n
|
||||||
|
mat-button
|
||||||
|
[routerLink]="['/portfolio', 'transactions']"
|
||||||
|
>Manage Transactions...</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
<div
|
<div
|
||||||
*ngIf="hasPositions === false"
|
*ngIf="hasPositions === false"
|
||||||
class="d-flex justify-content-center"
|
class="d-flex justify-content-center"
|
||||||
|
@ -5,9 +5,9 @@ import { MatCardModule } from '@angular/material/card';
|
|||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/client/components/no-transactions-info/no-transactions-info.module';
|
|
||||||
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||||
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
import { ZenPageRoutingModule } from './zen-page-routing.module';
|
import { ZenPageRoutingModule } from './zen-page-routing.module';
|
||||||
import { ZenPageComponent } from './zen-page.component';
|
import { ZenPageComponent } from './zen-page.component';
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
|
margin-bottom: env(safe-area-inset-bottom);
|
||||||
|
margin-bottom: constant(safe-area-inset-bottom);
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-tab-body-wrapper {
|
.mat-tab-body-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -27,7 +27,10 @@
|
|||||||
name="twitter:title"
|
name="twitter:title"
|
||||||
content="Ghostfolio – Open Source Wealth Management Software"
|
content="Ghostfolio – Open Source Wealth Management Software"
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="initial-scale=1, viewport-fit=cover, width=device-width"
|
||||||
|
/>
|
||||||
<meta property="og:description" content="" />
|
<meta property="og:description" content="" />
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
|
17
apps/ui-e2e/.eslintrc.json
Normal file
17
apps/ui-e2e/.eslintrc.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["src/plugins/index.js"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"no-undef": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
13
apps/ui-e2e/cypress.json
Normal file
13
apps/ui-e2e/cypress.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"fileServerFolder": ".",
|
||||||
|
"fixturesFolder": "./src/fixtures",
|
||||||
|
"integrationFolder": "./src/integration",
|
||||||
|
"modifyObstructiveCode": false,
|
||||||
|
"supportFile": "./src/support/index.ts",
|
||||||
|
"pluginsFile": "./src/plugins/index",
|
||||||
|
"video": true,
|
||||||
|
"videosFolder": "../../dist/cypress/apps/ui-e2e/videos",
|
||||||
|
"screenshotsFolder": "../../dist/cypress/apps/ui-e2e/screenshots",
|
||||||
|
"chromeWebSecurity": false,
|
||||||
|
"baseUrl": "http://localhost:4400"
|
||||||
|
}
|
4
apps/ui-e2e/src/fixtures/example.json
Normal file
4
apps/ui-e2e/src/fixtures/example.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "Using fixtures to represent data",
|
||||||
|
"email": "hello@cypress.io"
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
describe('ui', () => {
|
||||||
|
beforeEach(() => cy.visit('/iframe.html?id=valuecomponent--loading'));
|
||||||
|
it('should render the component', () => {
|
||||||
|
cy.get('gf-value').should('exist');
|
||||||
|
});
|
||||||
|
});
|
22
apps/ui-e2e/src/plugins/index.js
Normal file
22
apps/ui-e2e/src/plugins/index.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor');
|
||||||
|
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
|
||||||
|
// Preprocess Typescript file using Nx helper
|
||||||
|
on('file:preprocessor', preprocessTypescript(config));
|
||||||
|
};
|
33
apps/ui-e2e/src/support/commands.ts
Normal file
33
apps/ui-e2e/src/support/commands.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
declare namespace Cypress {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
interface Chainable<Subject> {
|
||||||
|
login(email: string, password: string): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
Cypress.Commands.add('login', (email, password) => {
|
||||||
|
console.log('Custom command example: Login', email, password);
|
||||||
|
});
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
17
apps/ui-e2e/src/support/index.ts
Normal file
17
apps/ui-e2e/src/support/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands';
|
10
apps/ui-e2e/tsconfig.json
Normal file
10
apps/ui-e2e/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": false,
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"allowJs": true,
|
||||||
|
"types": ["cypress", "node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.js"]
|
||||||
|
}
|
@ -1,7 +1,3 @@
|
|||||||
module.exports = {
|
const { getJestProjects } = require('@nrwl/jest');
|
||||||
projects: [
|
|
||||||
'<rootDir>/apps/api',
|
module.exports = { projects: getJestProjects() };
|
||||||
'<rootDir>/apps/client',
|
|
||||||
'<rootDir>/libs/common'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
@ -13,7 +13,7 @@ export const currencyPairs: Partial<
|
|||||||
currency1: Currency;
|
currency1: Currency;
|
||||||
currency2: Currency;
|
currency2: Currency;
|
||||||
}
|
}
|
||||||
>[] = Object.keys(Currency)
|
>[] = (Object.keys(Currency) as Array<keyof typeof Currency>)
|
||||||
.filter((currency) => {
|
.filter((currency) => {
|
||||||
return currency !== Currency.USD;
|
return currency !== Currency.USD;
|
||||||
})
|
})
|
||||||
@ -28,6 +28,7 @@ export const currencyPairs: Partial<
|
|||||||
|
|
||||||
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
|
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
|
||||||
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
|
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
|
||||||
|
export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
|
||||||
|
|
||||||
export const locale = 'de-CH';
|
export const locale = 'de-CH';
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export interface PortfolioPerformance {
|
export interface PortfolioPerformance {
|
||||||
currentGrossPerformance: number;
|
currentGrossPerformance: number;
|
||||||
currentGrossPerformancePercent: number;
|
currentGrossPerformancePercent: number;
|
||||||
|
currentNetPerformance: number;
|
||||||
|
currentNetPerformancePercent: number;
|
||||||
currentValue: number;
|
currentValue: number;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export interface PortfolioPosition {
|
|||||||
allocationCurrent: number;
|
allocationCurrent: number;
|
||||||
allocationInvestment: number;
|
allocationInvestment: number;
|
||||||
assetClass?: AssetClass;
|
assetClass?: AssetClass;
|
||||||
assetSubClass?: AssetSubClass;
|
assetSubClass?: AssetSubClass | 'CASH';
|
||||||
countries: Country[];
|
countries: Country[];
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
@ -20,6 +20,8 @@ export interface PortfolioPosition {
|
|||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
marketState: MarketState;
|
marketState: MarketState;
|
||||||
name: string;
|
name: string;
|
||||||
|
netPerformance: number;
|
||||||
|
netPerformancePercent: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
sectors: Sector[];
|
sectors: Sector[];
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
|
@ -13,6 +13,8 @@ export interface Position {
|
|||||||
marketPrice?: number;
|
marketPrice?: number;
|
||||||
marketState?: MarketState;
|
marketState?: MarketState;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
netPerformance?: number;
|
||||||
|
netPerformancePercentage?: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export interface Statistics {
|
export interface Statistics {
|
||||||
activeUsers1d: number;
|
activeUsers1d: number;
|
||||||
activeUsers30d: number;
|
activeUsers30d: number;
|
||||||
|
gitHubContributors: number;
|
||||||
gitHubStargazers: number;
|
gitHubStargazers: number;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ export interface TimelinePosition {
|
|||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
|
netPerformance: Big;
|
||||||
|
netPerformancePercentage: Big;
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||||
import { DateRange } from './date-range.type';
|
import type { DateRange } from './date-range.type';
|
||||||
import { Granularity } from './granularity.type';
|
import type { Granularity } from './granularity.type';
|
||||||
import { OrderWithAccount } from './order-with-account.type';
|
import type { OrderWithAccount } from './order-with-account.type';
|
||||||
import { RequestWithUser } from './request-with-user.type';
|
import type { RequestWithUser } from './request-with-user.type';
|
||||||
|
|
||||||
export {
|
export type {
|
||||||
AccessWithGranteeUser,
|
AccessWithGranteeUser,
|
||||||
DateRange,
|
DateRange,
|
||||||
Granularity,
|
Granularity,
|
||||||
|
36
libs/ui/.eslintrc.json
Normal file
36
libs/ui/.eslintrc.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts"],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@nrwl/nx/angular",
|
||||||
|
"plugin:@angular-eslint/template/process-inline-templates"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@angular-eslint/directive-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "attribute",
|
||||||
|
"prefix": "gf",
|
||||||
|
"style": "camelCase"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@angular-eslint/component-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"prefix": "gf",
|
||||||
|
"style": "kebab-case"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.html"],
|
||||||
|
"extends": ["plugin:@nrwl/nx/angular-template"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
24
libs/ui/.storybook/main.js
Normal file
24
libs/ui/.storybook/main.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const rootMain = require('../../../.storybook/main');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...rootMain,
|
||||||
|
|
||||||
|
core: { ...rootMain.core, builder: 'webpack5' },
|
||||||
|
|
||||||
|
stories: [
|
||||||
|
...rootMain.stories,
|
||||||
|
'../src/lib/**/*.stories.mdx',
|
||||||
|
'../src/lib/**/*.stories.@(js|jsx|ts|tsx)'
|
||||||
|
],
|
||||||
|
addons: [...rootMain.addons],
|
||||||
|
webpackFinal: async (config, { configType }) => {
|
||||||
|
// apply any global webpack configs that might have been specified in .storybook/main.js
|
||||||
|
if (rootMain.webpackFinal) {
|
||||||
|
config = await rootMain.webpackFinal(config, { configType });
|
||||||
|
}
|
||||||
|
|
||||||
|
// add your own webpack tweaks if needed
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
};
|
8
libs/ui/.storybook/preview-head.html
Normal file
8
libs/ui/.storybook/preview-head.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="https://unpkg.com/ionicons@5.5.1/dist/ionicons/ionicons.esm.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
nomodule
|
||||||
|
src="https://unpkg.com/ionicons@5.5.1/dist/ionicons/ionicons.js"
|
||||||
|
></script>
|
3
libs/ui/.storybook/preview.js
Normal file
3
libs/ui/.storybook/preview.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import '@angular/localize/init';
|
||||||
|
|
||||||
|
import '!style-loader!css-loader!sass-loader!../../../apps/client/src/styles.scss';
|
9
libs/ui/.storybook/tsconfig.json
Normal file
9
libs/ui/.storybook/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"exclude": ["../**/*.spec.ts"],
|
||||||
|
"include": ["../src/**/*", "*.js"]
|
||||||
|
}
|
7
libs/ui/README.md
Normal file
7
libs/ui/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# ui
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test ui` to execute the unit tests.
|
20
libs/ui/jest.config.js
Normal file
20
libs/ui/jest.config.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
displayName: 'ui',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||||
|
stringifyContentPathRegex: '\\.(html|svg)$'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
coverageDirectory: '../../coverage/libs/ui',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|js|html)$': 'jest-preset-angular'
|
||||||
|
},
|
||||||
|
snapshotSerializers: [
|
||||||
|
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||||
|
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||||
|
'jest-preset-angular/build/serializers/html-comment'
|
||||||
|
]
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user