Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
d5392de7c9 | |||
0f72673ef4 | |||
641fe4e8f4 | |||
18e06bb6e6 | |||
5b588c2000 | |||
162d19fa44 | |||
4a815d2031 | |||
d2aeeb3e88 | |||
ba926ffcf2 | |||
5ea455b98b | |||
39f315aba0 | |||
df2dfc20a1 |
43
CHANGELOG.md
43
CHANGELOG.md
@ -5,6 +5,49 @@ 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.54.0 - 18.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the data source attribute to the symbol profile model
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Respected the data source attribute in the data provider service
|
||||||
|
- Respected the data source attribute in the symbol data endpoint
|
||||||
|
- Improved the search functionality of the data management (multiple data sources)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Hid the net performance in the _Presenter View_ (portfolio holdings and summary tab on the home page)
|
||||||
|
- Hid the sign if the performance is zero in the value component
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:push`)
|
||||||
|
|
||||||
|
## 1.53.0 - 13.09.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the annualized performance calculation
|
||||||
|
- Changed the data gathering selection from distinct orders to symbol profiles
|
||||||
|
|
||||||
|
## 1.52.0 - 11.09.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the annualized performance to the portfolio summary tab on the home page
|
||||||
|
- Added the Ghostfolio Slack channel to the about page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `3.0.0` to `4.1.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the sign in with fingerprint for some android devices
|
||||||
|
|
||||||
## 1.51.0 - 11.09.2021
|
## 1.51.0 - 11.09.2021
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -128,7 +128,7 @@ Run `yarn test`
|
|||||||
|
|
||||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||||
|
|
||||||
Not sure what to work on? We have got some ideas. Please tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -62,10 +62,10 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('webauthn/generate-attestation-options')
|
@Get('webauthn/generate-registration-options')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async generateAttestationOptions() {
|
public async generateRegistrationOptions() {
|
||||||
return this.webAuthService.generateAttestationOptions();
|
return this.webAuthService.generateRegistrationOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('webauthn/verify-attestation')
|
@Post('webauthn/verify-attestation')
|
||||||
|
@ -11,16 +11,16 @@ import {
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import {
|
import {
|
||||||
GenerateAssertionOptionsOpts,
|
GenerateAuthenticationOptionsOpts,
|
||||||
GenerateAttestationOptionsOpts,
|
GenerateRegistrationOptionsOpts,
|
||||||
VerifiedAssertion,
|
VerifiedAuthenticationResponse,
|
||||||
VerifiedAttestation,
|
VerifiedRegistrationResponse,
|
||||||
VerifyAssertionResponseOpts,
|
VerifyAuthenticationResponseOpts,
|
||||||
VerifyAttestationResponseOpts,
|
VerifyRegistrationResponseOpts,
|
||||||
generateAssertionOptions,
|
generateAuthenticationOptions,
|
||||||
generateAttestationOptions,
|
generateRegistrationOptions,
|
||||||
verifyAssertionResponse,
|
verifyAuthenticationResponse,
|
||||||
verifyAttestationResponse
|
verifyRegistrationResponse
|
||||||
} from '@simplewebauthn/server';
|
} from '@simplewebauthn/server';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -46,10 +46,10 @@ export class WebAuthService {
|
|||||||
return this.configurationService.get('ROOT_URL');
|
return this.configurationService.get('ROOT_URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async generateAttestationOptions() {
|
public async generateRegistrationOptions() {
|
||||||
const user = this.request.user;
|
const user = this.request.user;
|
||||||
|
|
||||||
const opts: GenerateAttestationOptionsOpts = {
|
const opts: GenerateRegistrationOptionsOpts = {
|
||||||
rpName: 'Ghostfolio',
|
rpName: 'Ghostfolio',
|
||||||
rpID: this.rpID,
|
rpID: this.rpID,
|
||||||
userID: user.id,
|
userID: user.id,
|
||||||
@ -63,7 +63,7 @@ export class WebAuthService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = generateAttestationOptions(opts);
|
const options = generateRegistrationOptions(opts);
|
||||||
|
|
||||||
await this.userService.updateUser({
|
await this.userService.updateUser({
|
||||||
data: {
|
data: {
|
||||||
@ -84,27 +84,27 @@ export class WebAuthService {
|
|||||||
const user = this.request.user;
|
const user = this.request.user;
|
||||||
const expectedChallenge = user.authChallenge;
|
const expectedChallenge = user.authChallenge;
|
||||||
|
|
||||||
let verification: VerifiedAttestation;
|
let verification: VerifiedRegistrationResponse;
|
||||||
try {
|
try {
|
||||||
const opts: VerifyAttestationResponseOpts = {
|
const opts: VerifyRegistrationResponseOpts = {
|
||||||
credential,
|
credential,
|
||||||
expectedChallenge,
|
expectedChallenge,
|
||||||
expectedOrigin: this.expectedOrigin,
|
expectedOrigin: this.expectedOrigin,
|
||||||
expectedRPID: this.rpID
|
expectedRPID: this.rpID
|
||||||
};
|
};
|
||||||
verification = await verifyAttestationResponse(opts);
|
verification = await verifyRegistrationResponse(opts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new InternalServerErrorException(error.message);
|
throw new InternalServerErrorException(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { verified, attestationInfo } = verification;
|
const { registrationInfo, verified } = verification;
|
||||||
|
|
||||||
const devices = await this.deviceService.authDevices({
|
const devices = await this.deviceService.authDevices({
|
||||||
where: { userId: user.id }
|
where: { userId: user.id }
|
||||||
});
|
});
|
||||||
if (verified && attestationInfo) {
|
if (registrationInfo && verified) {
|
||||||
const { credentialPublicKey, credentialID, counter } = attestationInfo;
|
const { counter, credentialID, credentialPublicKey } = registrationInfo;
|
||||||
|
|
||||||
let existingDevice = devices.find(
|
let existingDevice = devices.find(
|
||||||
(device) => device.credentialId === credentialID
|
(device) => device.credentialId === credentialID
|
||||||
@ -115,9 +115,9 @@ export class WebAuthService {
|
|||||||
* Add the returned device to the user's list of devices
|
* Add the returned device to the user's list of devices
|
||||||
*/
|
*/
|
||||||
existingDevice = await this.deviceService.createAuthDevice({
|
existingDevice = await this.deviceService.createAuthDevice({
|
||||||
|
counter,
|
||||||
credentialPublicKey,
|
credentialPublicKey,
|
||||||
credentialId: credentialID,
|
credentialId: credentialID,
|
||||||
counter,
|
|
||||||
User: { connect: { id: user.id } }
|
User: { connect: { id: user.id } }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -138,20 +138,20 @@ export class WebAuthService {
|
|||||||
throw new Error('Device not found');
|
throw new Error('Device not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts: GenerateAssertionOptionsOpts = {
|
const opts: GenerateAuthenticationOptionsOpts = {
|
||||||
timeout: 60000,
|
|
||||||
allowCredentials: [
|
allowCredentials: [
|
||||||
{
|
{
|
||||||
id: device.credentialId,
|
id: device.credentialId,
|
||||||
type: 'public-key',
|
transports: ['internal'],
|
||||||
transports: ['internal']
|
type: 'public-key'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
userVerification: 'preferred',
|
rpID: this.rpID,
|
||||||
rpID: this.rpID
|
timeout: 60000,
|
||||||
|
userVerification: 'preferred'
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = generateAssertionOptions(opts);
|
const options = generateAuthenticationOptions(opts);
|
||||||
|
|
||||||
await this.userService.updateUser({
|
await this.userService.updateUser({
|
||||||
data: {
|
data: {
|
||||||
@ -177,29 +177,29 @@ export class WebAuthService {
|
|||||||
|
|
||||||
const user = await this.userService.user({ id: device.userId });
|
const user = await this.userService.user({ id: device.userId });
|
||||||
|
|
||||||
let verification: VerifiedAssertion;
|
let verification: VerifiedAuthenticationResponse;
|
||||||
try {
|
try {
|
||||||
const opts: VerifyAssertionResponseOpts = {
|
const opts: VerifyAuthenticationResponseOpts = {
|
||||||
credential,
|
credential,
|
||||||
expectedChallenge: `${user.authChallenge}`,
|
|
||||||
expectedOrigin: this.expectedOrigin,
|
|
||||||
expectedRPID: this.rpID,
|
|
||||||
authenticator: {
|
authenticator: {
|
||||||
credentialID: device.credentialId,
|
credentialID: device.credentialId,
|
||||||
credentialPublicKey: device.credentialPublicKey,
|
credentialPublicKey: device.credentialPublicKey,
|
||||||
counter: device.counter
|
counter: device.counter
|
||||||
}
|
},
|
||||||
|
expectedChallenge: `${user.authChallenge}`,
|
||||||
|
expectedOrigin: this.expectedOrigin,
|
||||||
|
expectedRPID: this.rpID
|
||||||
};
|
};
|
||||||
verification = verifyAssertionResponse(opts);
|
verification = verifyAuthenticationResponse(opts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new InternalServerErrorException({ error: error.message });
|
throw new InternalServerErrorException({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { verified, assertionInfo } = verification;
|
const { verified, authenticationInfo } = verification;
|
||||||
|
|
||||||
if (verified) {
|
if (verified) {
|
||||||
device.counter = assertionInfo.newCounter;
|
device.counter = authenticationInfo.newCounter;
|
||||||
|
|
||||||
await this.deviceService.updateAuthDevice({
|
await this.deviceService.updateAuthDevice({
|
||||||
data: device,
|
data: device,
|
||||||
|
@ -56,7 +56,9 @@ export class OrderService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataGatheringService.gatherProfileData([data.symbol]);
|
this.dataGatheringService.gatherProfileData([
|
||||||
|
{ dataSource: data.dataSource, symbol: data.symbol }
|
||||||
|
]);
|
||||||
|
|
||||||
await this.cacheService.flush();
|
await this.cacheService.flush();
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { Currency, MarketData } from '@prisma/client';
|
import { Currency, DataSource, MarketData } from '@prisma/client';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { MarketDataService } from './market-data.service';
|
import { MarketDataService } from './market-data.service';
|
||||||
@ -14,6 +14,7 @@ jest.mock('./market-data.service', () => {
|
|||||||
date,
|
date,
|
||||||
symbol,
|
symbol,
|
||||||
createdAt: date,
|
createdAt: date,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
id: 'aefcbe3a-ee10-4c4f-9f2d-8ffad7b05584',
|
id: 'aefcbe3a-ee10-4c4f-9f2d-8ffad7b05584',
|
||||||
marketPrice: 1847.839966
|
marketPrice: 1847.839966
|
||||||
});
|
});
|
||||||
@ -30,6 +31,7 @@ jest.mock('./market-data.service', () => {
|
|||||||
return Promise.resolve<MarketData[]>([
|
return Promise.resolve<MarketData[]>([
|
||||||
{
|
{
|
||||||
createdAt: dateRangeStart,
|
createdAt: dateRangeStart,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: dateRangeStart,
|
date: dateRangeStart,
|
||||||
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
|
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
|
||||||
marketPrice: 1841.823902,
|
marketPrice: 1841.823902,
|
||||||
@ -37,6 +39,7 @@ jest.mock('./market-data.service', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
createdAt: dateRangeEnd,
|
createdAt: dateRangeEnd,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: dateRangeEnd,
|
date: dateRangeEnd,
|
||||||
id: '082d6893-df27-4c91-8a5d-092e84315b56',
|
id: '082d6893-df27-4c91-8a5d-092e84315b56',
|
||||||
marketPrice: 1847.839966,
|
marketPrice: 1847.839966,
|
||||||
@ -106,11 +109,11 @@ describe('CurrentRateService', () => {
|
|||||||
expect(
|
expect(
|
||||||
await currentRateService.getValues({
|
await currentRateService.getValues({
|
||||||
currencies: { AMZN: Currency.USD },
|
currencies: { AMZN: Currency.USD },
|
||||||
|
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
|
||||||
dateQuery: {
|
dateQuery: {
|
||||||
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
|
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
|
||||||
gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0))
|
gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0))
|
||||||
},
|
},
|
||||||
symbols: ['AMZN'],
|
|
||||||
userCurrency: Currency.CHF
|
userCurrency: Currency.CHF
|
||||||
})
|
})
|
||||||
).toMatchObject([
|
).toMatchObject([
|
||||||
|
@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { isBefore, isToday } from 'date-fns';
|
import { isBefore, isToday } from 'date-fns';
|
||||||
import { flatten } from 'lodash';
|
import { flatten } from 'lodash';
|
||||||
|
|
||||||
@ -25,7 +26,9 @@ export class CurrentRateService {
|
|||||||
userCurrency
|
userCurrency
|
||||||
}: GetValueParams): Promise<GetValueObject> {
|
}: GetValueParams): Promise<GetValueObject> {
|
||||||
if (isToday(date)) {
|
if (isToday(date)) {
|
||||||
const dataProviderResult = await this.dataProviderService.get([symbol]);
|
const dataProviderResult = await this.dataProviderService.get([
|
||||||
|
{ symbol, dataSource: DataSource.YAHOO }
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
date: resetHours(date),
|
date: resetHours(date),
|
||||||
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0,
|
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0,
|
||||||
@ -55,8 +58,8 @@ export class CurrentRateService {
|
|||||||
|
|
||||||
public async getValues({
|
public async getValues({
|
||||||
currencies,
|
currencies,
|
||||||
|
dataGatheringItems,
|
||||||
dateQuery,
|
dateQuery,
|
||||||
symbols,
|
|
||||||
userCurrency
|
userCurrency
|
||||||
}: GetValuesParams): Promise<GetValueObject[]> {
|
}: GetValuesParams): Promise<GetValueObject[]> {
|
||||||
const includeToday =
|
const includeToday =
|
||||||
@ -75,24 +78,31 @@ export class CurrentRateService {
|
|||||||
if (includeToday) {
|
if (includeToday) {
|
||||||
const today = resetHours(new Date());
|
const today = resetHours(new Date());
|
||||||
promises.push(
|
promises.push(
|
||||||
this.dataProviderService.get(symbols).then((dataResultProvider) => {
|
this.dataProviderService
|
||||||
const result = [];
|
.get(dataGatheringItems)
|
||||||
for (const symbol of symbols) {
|
.then((dataResultProvider) => {
|
||||||
result.push({
|
const result = [];
|
||||||
symbol,
|
for (const dataGatheringItem of dataGatheringItems) {
|
||||||
date: today,
|
result.push({
|
||||||
marketPrice: this.exchangeRateDataService.toCurrency(
|
date: today,
|
||||||
dataResultProvider?.[symbol]?.marketPrice ?? 0,
|
marketPrice: this.exchangeRateDataService.toCurrency(
|
||||||
dataResultProvider?.[symbol]?.currency,
|
dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice ??
|
||||||
userCurrency
|
0,
|
||||||
)
|
dataResultProvider?.[dataGatheringItem.symbol]?.currency,
|
||||||
});
|
userCurrency
|
||||||
}
|
),
|
||||||
return result;
|
symbol: dataGatheringItem.symbol
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
||||||
|
return dataGatheringItem.symbol;
|
||||||
|
});
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
this.marketDataService
|
this.marketDataService
|
||||||
.getRange({
|
.getRange({
|
||||||
|
@ -6,6 +6,7 @@ export interface CurrentPositions {
|
|||||||
positions: TimelinePosition[];
|
positions: TimelinePosition[];
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
|
netAnnualizedPerformance: Big;
|
||||||
netPerformance: Big;
|
netPerformance: Big;
|
||||||
netPerformancePercentage: Big;
|
netPerformancePercentage: Big;
|
||||||
currentValue: Big;
|
currentValue: Big;
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
import { DateQuery } from './date-query.interface';
|
import { DateQuery } from './date-query.interface';
|
||||||
|
|
||||||
export interface GetValuesParams {
|
export interface GetValuesParams {
|
||||||
currencies: { [symbol: string]: Currency };
|
currencies: { [symbol: string]: Currency };
|
||||||
|
dataGatheringItems: IDataGatheringItem[];
|
||||||
dateQuery: DateQuery;
|
dateQuery: DateQuery;
|
||||||
symbols: string[];
|
|
||||||
userCurrency: Currency;
|
userCurrency: Currency;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface PortfolioOrder {
|
export interface PortfolioOrder {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
date: string;
|
date: string;
|
||||||
|
dataSource: DataSource;
|
||||||
fee: Big;
|
fee: Big;
|
||||||
name: string;
|
name: string;
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface TransactionPointSymbol {
|
export interface TransactionPointSymbol {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
fee: Big;
|
fee: Big;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
@ -84,7 +85,7 @@ jest.mock('./current-rate.service', () => {
|
|||||||
getValues: ({
|
getValues: ({
|
||||||
currencies,
|
currencies,
|
||||||
dateQuery,
|
dateQuery,
|
||||||
symbols,
|
dataGatheringItems,
|
||||||
userCurrency
|
userCurrency
|
||||||
}: GetValuesParams) => {
|
}: GetValuesParams) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
@ -94,21 +95,23 @@ jest.mock('./current-rate.service', () => {
|
|||||||
isBefore(date, endOfDay(dateQuery.lt));
|
isBefore(date, endOfDay(dateQuery.lt));
|
||||||
date = addDays(date, 1)
|
date = addDays(date, 1)
|
||||||
) {
|
) {
|
||||||
for (const symbol of symbols) {
|
for (const dataGatheringItem of dataGatheringItems) {
|
||||||
result.push({
|
result.push({
|
||||||
date,
|
date,
|
||||||
symbol,
|
marketPrice: mockGetValue(dataGatheringItem.symbol, date)
|
||||||
marketPrice: mockGetValue(symbol, date).marketPrice
|
.marketPrice,
|
||||||
|
symbol: dataGatheringItem.symbol
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const date of dateQuery.in) {
|
for (const date of dateQuery.in) {
|
||||||
for (const symbol of symbols) {
|
for (const dataGatheringItem of dataGatheringItems) {
|
||||||
result.push({
|
result.push({
|
||||||
date,
|
date,
|
||||||
symbol,
|
marketPrice: mockGetValue(dataGatheringItem.symbol, date)
|
||||||
marketPrice: mockGetValue(symbol, date).marketPrice
|
.marketPrice,
|
||||||
|
symbol: dataGatheringItem.symbol
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,7 +150,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
Currency.USD
|
||||||
);
|
);
|
||||||
const orders = [
|
const orders: PortfolioOrder[] = [
|
||||||
{
|
{
|
||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
||||||
@ -156,6 +159,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('144.38'),
|
unitPrice: new Big('144.38'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -166,6 +170,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('147.99'),
|
unitPrice: new Big('147.99'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('10')
|
fee: new Big('10')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -176,6 +181,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
type: OrderType.Sell,
|
type: OrderType.Sell,
|
||||||
unitPrice: new Big('151.41'),
|
unitPrice: new Big('151.41'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -188,6 +194,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
@ -202,6 +209,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-08-03',
|
date: '2019-08-03',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
@ -216,6 +224,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2020-02-02',
|
date: '2020-02-02',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
@ -234,7 +243,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
Currency.USD
|
||||||
);
|
);
|
||||||
const orders = [
|
const orders: PortfolioOrder[] = [
|
||||||
{
|
{
|
||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
||||||
@ -243,6 +252,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('144.38'),
|
unitPrice: new Big('144.38'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -253,6 +263,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('147.99'),
|
unitPrice: new Big('147.99'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('10')
|
fee: new Big('10')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -263,6 +274,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
type: OrderType.Sell,
|
type: OrderType.Sell,
|
||||||
unitPrice: new Big('151.41'),
|
unitPrice: new Big('151.41'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -275,6 +287,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
@ -289,6 +302,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-08-03',
|
date: '2019-08-03',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
@ -298,6 +312,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
fee: new Big('5')
|
fee: new Big('5')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTX',
|
symbol: 'VTX',
|
||||||
investment: new Big('1479.9'),
|
investment: new Big('1479.9'),
|
||||||
@ -312,6 +327,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2020-02-02',
|
date: '2020-02-02',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('686.75'),
|
investment: new Big('686.75'),
|
||||||
@ -321,6 +337,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
fee: new Big('10')
|
fee: new Big('10')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTX',
|
symbol: 'VTX',
|
||||||
investment: new Big('1479.9'),
|
investment: new Big('1479.9'),
|
||||||
@ -335,10 +352,11 @@ describe('PortfolioCalculator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with two orders at the same day of the same type', () => {
|
it('with two orders at the same day of the same type', () => {
|
||||||
const orders = [
|
const orders: PortfolioOrder[] = [
|
||||||
...ordersVTI,
|
...ordersVTI,
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: '2021-02-01',
|
date: '2021-02-01',
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
@ -362,6 +380,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
@ -376,6 +395,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
@ -390,6 +410,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
@ -404,6 +425,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('6627.05'),
|
investment: new Big('6627.05'),
|
||||||
quantity: new Big('35'),
|
quantity: new Big('35'),
|
||||||
@ -418,6 +440,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
investment: new Big('8403.95'),
|
investment: new Big('8403.95'),
|
||||||
quantity: new Big('45'),
|
quantity: new Big('45'),
|
||||||
@ -431,10 +454,11 @@ describe('PortfolioCalculator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with additional order', () => {
|
it('with additional order', () => {
|
||||||
const orders = [
|
const orders: PortfolioOrder[] = [
|
||||||
...ordersVTI,
|
...ordersVTI,
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: '2019-09-01',
|
date: '2019-09-01',
|
||||||
name: 'Amazon.com, Inc.',
|
name: 'Amazon.com, Inc.',
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
@ -457,6 +481,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
@ -471,6 +496,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-08-03',
|
date: '2019-08-03',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
@ -485,6 +511,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2019-09-01',
|
date: '2019-09-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -494,6 +521,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
@ -508,6 +536,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2020-02-02',
|
date: '2020-02-02',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -517,6 +546,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
@ -531,6 +561,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2021-02-01',
|
date: '2021-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -540,6 +571,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('15'),
|
quantity: new Big('15'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
@ -554,6 +586,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2021-08-01',
|
date: '2021-08-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -563,6 +596,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('25'),
|
quantity: new Big('25'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
@ -577,7 +611,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with additional buy & sell', () => {
|
it('with additional buy & sell', () => {
|
||||||
const orders = [
|
const orders: PortfolioOrder[] = [
|
||||||
...ordersVTI,
|
...ordersVTI,
|
||||||
{
|
{
|
||||||
date: '2019-09-01',
|
date: '2019-09-01',
|
||||||
@ -587,6 +621,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('2021.99'),
|
unitPrice: new Big('2021.99'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -597,6 +632,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
type: OrderType.Sell,
|
type: OrderType.Sell,
|
||||||
unitPrice: new Big('2412.23'),
|
unitPrice: new Big('2412.23'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -627,6 +663,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2017-01-03',
|
date: '2017-01-03',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
@ -641,6 +678,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2017-07-01',
|
date: '2017-07-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('0.5614682'),
|
quantity: new Big('0.5614682'),
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
investment: new Big('1999.9999999999998659756'),
|
investment: new Big('1999.9999999999998659756'),
|
||||||
@ -650,6 +688,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
@ -664,6 +703,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
date: '2018-09-01',
|
date: '2018-09-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('5'),
|
quantity: new Big('5'),
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
@ -673,6 +713,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('0.5614682'),
|
quantity: new Big('0.5614682'),
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
investment: new Big('1999.9999999999998659756'),
|
investment: new Big('1999.9999999999998659756'),
|
||||||
@ -682,6 +723,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
quantity: new Big('50'),
|
quantity: new Big('50'),
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('2148.5'),
|
investment: new Big('2148.5'),
|
||||||
@ -928,6 +970,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('805.9'),
|
investment: new Big('805.9'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -942,6 +985,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('0'),
|
investment: new Big('0'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -956,6 +1000,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1013.9'),
|
investment: new Big('1013.9'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 3
|
transactionCount: 3
|
||||||
@ -1004,16 +1049,16 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
Currency.USD
|
||||||
);
|
);
|
||||||
const transactionPoints = [
|
const transactionPoints: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1025,10 +1070,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -1087,16 +1132,16 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
Currency.USD
|
||||||
);
|
);
|
||||||
const transactionPoints = [
|
const transactionPoints: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(50),
|
fee: new Big(50),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1108,10 +1153,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(100),
|
fee: new Big(100),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -1147,12 +1192,14 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('3897.2'),
|
currentValue: new Big('3897.2'),
|
||||||
grossPerformance: new Big('303.2'),
|
grossPerformance: new Big('303.2'),
|
||||||
grossPerformancePercentage: new Big('0.27537838148272398344'),
|
grossPerformancePercentage: new Big('0.27537838148272398344'),
|
||||||
|
netAnnualizedPerformance: new Big('0.1412977563032074'),
|
||||||
netPerformance: new Big('253.2'),
|
netPerformance: new Big('253.2'),
|
||||||
netPerformancePercentage: new Big('0.2566937088951485493'),
|
netPerformancePercentage: new Big('0.2566937088951485493'),
|
||||||
totalInvestment: new Big('2923.7'),
|
totalInvestment: new Big('2923.7'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('146.185'),
|
averagePrice: new Big('146.185'),
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
@ -1178,16 +1225,16 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
Currency.USD
|
||||||
);
|
);
|
||||||
const transactionPoints = [
|
const transactionPoints: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
quantity: new Big('10'),
|
quantity: new Big('10'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(50),
|
fee: new Big(50),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1199,10 +1246,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
quantity: new Big('20'),
|
quantity: new Big('20'),
|
||||||
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
|
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(100),
|
fee: new Big(100),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -1275,6 +1322,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'MFA', // Mutual Fund A
|
symbol: 'MFA', // Mutual Fund A
|
||||||
investment: new Big('1000000'), // 1 million
|
investment: new Big('1000000'), // 1 million
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2010-12-31',
|
firstBuyDate: '2010-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1289,6 +1337,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'MFA', // Mutual Fund A
|
symbol: 'MFA', // Mutual Fund A
|
||||||
investment: new Big('1100000'), // 1,000,000 + 100,000
|
investment: new Big('1100000'), // 1,000,000 + 100,000
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2010-12-31',
|
firstBuyDate: '2010-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -1350,6 +1399,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'SPA', // Sub Portfolio A
|
symbol: 'SPA', // Sub Portfolio A
|
||||||
investment: new Big('200'),
|
investment: new Big('200'),
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1359,6 +1409,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'SPB', // Sub Portfolio B
|
symbol: 'SPB', // Sub Portfolio B
|
||||||
investment: new Big('300'),
|
investment: new Big('300'),
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1373,6 +1424,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'SPA', // Sub Portfolio A
|
symbol: 'SPA', // Sub Portfolio A
|
||||||
investment: new Big('200'),
|
investment: new Big('200'),
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1382,6 +1434,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'SPB', // Sub Portfolio B
|
symbol: 'SPB', // Sub Portfolio B
|
||||||
investment: new Big('300'),
|
investment: new Big('300'),
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2012-12-31',
|
firstBuyDate: '2012-12-31',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1486,7 +1539,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentRateService,
|
currentRateService,
|
||||||
Currency.USD
|
Currency.USD
|
||||||
);
|
);
|
||||||
const transactionPoints = [
|
const transactionPoints: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
@ -1495,6 +1548,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(50),
|
fee: new Big(50),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -1509,6 +1563,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(100),
|
fee: new Big(100),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -1523,6 +1578,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(150),
|
fee: new Big(150),
|
||||||
transactionCount: 3
|
transactionCount: 3
|
||||||
@ -1537,6 +1593,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(200),
|
fee: new Big(200),
|
||||||
transactionCount: 4
|
transactionCount: 4
|
||||||
@ -1551,6 +1608,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(250),
|
fee: new Big(250),
|
||||||
transactionCount: 5
|
transactionCount: 5
|
||||||
@ -2215,6 +2273,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -2224,6 +2283,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -2261,6 +2321,66 @@ describe('PortfolioCalculator', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('annualized performance percentage', () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
|
currentRateService,
|
||||||
|
Currency.USD
|
||||||
|
);
|
||||||
|
|
||||||
|
it('Get annualized performance', async () => {
|
||||||
|
expect(
|
||||||
|
portfolioCalculator
|
||||||
|
.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
||||||
|
netPerformancePercent: new Big(0)
|
||||||
|
})
|
||||||
|
.toNumber()
|
||||||
|
).toEqual(0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
portfolioCalculator
|
||||||
|
.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket: 0,
|
||||||
|
netPerformancePercent: new Big(0)
|
||||||
|
})
|
||||||
|
.toNumber()
|
||||||
|
).toEqual(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source: https://www.readyratios.com/reference/analysis/annualized_rate.html
|
||||||
|
*/
|
||||||
|
expect(
|
||||||
|
portfolioCalculator
|
||||||
|
.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket: 65, // < 1 year
|
||||||
|
netPerformancePercent: new Big(0.1025)
|
||||||
|
})
|
||||||
|
.toNumber()
|
||||||
|
).toBeCloseTo(0.729705);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
portfolioCalculator
|
||||||
|
.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket: 365, // 1 year
|
||||||
|
netPerformancePercent: new Big(0.05)
|
||||||
|
})
|
||||||
|
.toNumber()
|
||||||
|
).toBeCloseTo(0.05);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation
|
||||||
|
*/
|
||||||
|
expect(
|
||||||
|
portfolioCalculator
|
||||||
|
.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket: 575, // > 1 year
|
||||||
|
netPerformancePercent: new Big(0.2374)
|
||||||
|
})
|
||||||
|
.toNumber()
|
||||||
|
).toBeCloseTo(0.145);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const ordersMixedSymbols: PortfolioOrder[] = [
|
const ordersMixedSymbols: PortfolioOrder[] = [
|
||||||
@ -2272,6 +2392,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('42.97'),
|
unitPrice: new Big('42.97'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2282,6 +2403,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('3562.089535970158'),
|
unitPrice: new Big('3562.089535970158'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2292,6 +2414,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('2021.99'),
|
unitPrice: new Big('2021.99'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -2305,6 +2428,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('144.38'),
|
unitPrice: new Big('144.38'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2315,6 +2439,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('147.99'),
|
unitPrice: new Big('147.99'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2325,6 +2450,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
type: OrderType.Sell,
|
type: OrderType.Sell,
|
||||||
unitPrice: new Big('151.41'),
|
unitPrice: new Big('151.41'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2335,6 +2461,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('177.69'),
|
unitPrice: new Big('177.69'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2345,6 +2472,7 @@ const ordersVTI: PortfolioOrder[] = [
|
|||||||
type: OrderType.Buy,
|
type: OrderType.Buy,
|
||||||
unitPrice: new Big('203.15'),
|
unitPrice: new Big('203.15'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: new Big(0)
|
fee: new Big(0)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -2358,6 +2486,7 @@ const orderTslaTransactionPoint: TransactionPoint[] = [
|
|||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
investment: new Big('719.46'),
|
investment: new Big('719.46'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2021-01-01',
|
firstBuyDate: '2021-01-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -2375,6 +2504,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -2389,6 +2519,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -2403,6 +2534,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 3
|
transactionCount: 3
|
||||||
@ -2417,6 +2549,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 4
|
transactionCount: 4
|
||||||
@ -2431,6 +2564,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 5
|
transactionCount: 5
|
||||||
@ -2439,7 +2573,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const transactionPointsBuyAndSell = [
|
const transactionPointsBuyAndSell: TransactionPoint[] = [
|
||||||
{
|
{
|
||||||
date: '2019-02-01',
|
date: '2019-02-01',
|
||||||
items: [
|
items: [
|
||||||
@ -2448,6 +2582,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('1443.8'),
|
investment: new Big('1443.8'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -2462,6 +2597,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -2476,6 +2612,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -2485,6 +2622,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2923.7'),
|
investment: new Big('2923.7'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -2499,6 +2637,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('10109.95'),
|
investment: new Big('10109.95'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
@ -2508,6 +2647,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 3
|
transactionCount: 3
|
||||||
@ -2522,6 +2662,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('0'),
|
investment: new Big('0'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -2531,6 +2672,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('652.55'),
|
investment: new Big('652.55'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 3
|
transactionCount: 3
|
||||||
@ -2545,6 +2687,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('0'),
|
investment: new Big('0'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -2554,6 +2697,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('2684.05'),
|
investment: new Big('2684.05'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 4
|
transactionCount: 4
|
||||||
@ -2568,6 +2712,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
investment: new Big('0'),
|
investment: new Big('0'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-09-01',
|
firstBuyDate: '2019-09-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
@ -2577,6 +2722,7 @@ const transactionPointsBuyAndSell = [
|
|||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
investment: new Big('4460.95'),
|
investment: new Big('4460.95'),
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
firstBuyDate: '2019-02-01',
|
firstBuyDate: '2019-02-01',
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
transactionCount: 5
|
transactionCount: 5
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
addMonths,
|
addMonths,
|
||||||
addYears,
|
addYears,
|
||||||
|
differenceInDays,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
@ -14,7 +16,7 @@ import {
|
|||||||
max,
|
max,
|
||||||
min
|
min
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { flatten } from 'lodash';
|
import { flatten, isNumber } from 'lodash';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { CurrentPositions } from './interfaces/current-positions.interface';
|
import { CurrentPositions } from './interfaces/current-positions.interface';
|
||||||
@ -58,6 +60,7 @@ export class PortfolioCalculator {
|
|||||||
.plus(oldAccumulatedSymbol.quantity);
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
dataSource: order.dataSource,
|
||||||
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
investment: newQuantity.eq(0)
|
investment: newQuantity.eq(0)
|
||||||
@ -73,6 +76,7 @@ export class PortfolioCalculator {
|
|||||||
} else {
|
} else {
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
dataSource: order.dataSource,
|
||||||
fee: order.fee,
|
fee: order.fee,
|
||||||
firstBuyDate: order.date,
|
firstBuyDate: order.date,
|
||||||
investment: unitPrice.mul(order.quantity).mul(factor),
|
investment: unitPrice.mul(order.quantity).mul(factor),
|
||||||
@ -103,6 +107,23 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket,
|
||||||
|
netPerformancePercent
|
||||||
|
}: {
|
||||||
|
daysInMarket: number;
|
||||||
|
netPerformancePercent: Big;
|
||||||
|
}): Big {
|
||||||
|
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||||
|
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||||
|
return new Big(
|
||||||
|
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
|
||||||
|
).minus(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Big(0);
|
||||||
|
}
|
||||||
|
|
||||||
public getTransactionPoints(): TransactionPoint[] {
|
public getTransactionPoints(): TransactionPoint[] {
|
||||||
return this.transactionPoints;
|
return this.transactionPoints;
|
||||||
}
|
}
|
||||||
@ -118,6 +139,7 @@ export class PortfolioCalculator {
|
|||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
|
netAnnualizedPerformance: new Big(0),
|
||||||
netPerformance: new Big(0),
|
netPerformance: new Big(0),
|
||||||
netPerformancePercentage: new Big(0),
|
netPerformancePercentage: new Big(0),
|
||||||
positions: [],
|
positions: [],
|
||||||
@ -134,12 +156,15 @@ export class PortfolioCalculator {
|
|||||||
let firstTransactionPoint: TransactionPoint = null;
|
let firstTransactionPoint: TransactionPoint = null;
|
||||||
let firstIndex = this.transactionPoints.length;
|
let firstIndex = this.transactionPoints.length;
|
||||||
const dates = [];
|
const dates = [];
|
||||||
const symbols = new Set<string>();
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
const currencies: { [symbol: string]: Currency } = {};
|
const currencies: { [symbol: string]: Currency } = {};
|
||||||
|
|
||||||
dates.push(resetHours(start));
|
dates.push(resetHours(start));
|
||||||
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
||||||
symbols.add(item.symbol);
|
dataGatheringItems.push({
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
symbol: item.symbol
|
||||||
|
});
|
||||||
currencies[item.symbol] = item.currency;
|
currencies[item.symbol] = item.currency;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.transactionPoints.length; i++) {
|
for (let i = 0; i < this.transactionPoints.length; i++) {
|
||||||
@ -159,10 +184,10 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
const marketSymbols = await this.currentRateService.getValues({
|
const marketSymbols = await this.currentRateService.getValues({
|
||||||
currencies,
|
currencies,
|
||||||
|
dataGatheringItems,
|
||||||
dateQuery: {
|
dateQuery: {
|
||||||
in: dates
|
in: dates
|
||||||
},
|
},
|
||||||
symbols: Array.from(symbols),
|
|
||||||
userCurrency: this.currency
|
userCurrency: this.currency
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -290,6 +315,7 @@ export class PortfolioCalculator {
|
|||||||
? new Big(0)
|
? new Big(0)
|
||||||
: item.investment.div(item.quantity),
|
: item.investment.div(item.quantity),
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
|
dataSource: item.dataSource,
|
||||||
firstBuyDate: item.firstBuyDate,
|
firstBuyDate: item.firstBuyDate,
|
||||||
grossPerformance: isValid
|
grossPerformance: isValid
|
||||||
? grossPerformance[item.symbol] ?? null
|
? grossPerformance[item.symbol] ?? null
|
||||||
@ -410,6 +436,11 @@ export class PortfolioCalculator {
|
|||||||
let netPerformance = new Big(0);
|
let netPerformance = new Big(0);
|
||||||
let netPerformancePercentage = new Big(0);
|
let netPerformancePercentage = new Big(0);
|
||||||
let completeInitialValue = new Big(0);
|
let completeInitialValue = new Big(0);
|
||||||
|
let netAnnualizedPerformance = new Big(0);
|
||||||
|
|
||||||
|
// use Date.now() to use the mock for today
|
||||||
|
const today = new Date(Date.now());
|
||||||
|
|
||||||
for (const currentPosition of positions) {
|
for (const currentPosition of positions) {
|
||||||
if (currentPosition.marketPrice) {
|
if (currentPosition.marketPrice) {
|
||||||
currentValue = currentValue.add(
|
currentValue = currentValue.add(
|
||||||
@ -437,6 +468,15 @@ export class PortfolioCalculator {
|
|||||||
grossPerformancePercentage = grossPerformancePercentage.plus(
|
grossPerformancePercentage = grossPerformancePercentage.plus(
|
||||||
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
||||||
);
|
);
|
||||||
|
netAnnualizedPerformance = netAnnualizedPerformance.plus(
|
||||||
|
this.getAnnualizedPerformancePercent({
|
||||||
|
daysInMarket: differenceInDays(
|
||||||
|
today,
|
||||||
|
parseDate(currentPosition.firstBuyDate)
|
||||||
|
),
|
||||||
|
netPerformancePercent: currentPosition.netPerformancePercentage
|
||||||
|
}).mul(currentInitialValue)
|
||||||
|
);
|
||||||
netPerformancePercentage = netPerformancePercentage.plus(
|
netPerformancePercentage = netPerformancePercentage.plus(
|
||||||
currentPosition.netPerformancePercentage.mul(currentInitialValue)
|
currentPosition.netPerformancePercentage.mul(currentInitialValue)
|
||||||
);
|
);
|
||||||
@ -453,6 +493,8 @@ export class PortfolioCalculator {
|
|||||||
grossPerformancePercentage.div(completeInitialValue);
|
grossPerformancePercentage.div(completeInitialValue);
|
||||||
netPerformancePercentage =
|
netPerformancePercentage =
|
||||||
netPerformancePercentage.div(completeInitialValue);
|
netPerformancePercentage.div(completeInitialValue);
|
||||||
|
netAnnualizedPerformance =
|
||||||
|
netAnnualizedPerformance.div(completeInitialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -460,6 +502,7 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
|
netAnnualizedPerformance,
|
||||||
netPerformance,
|
netPerformance,
|
||||||
netPerformancePercentage,
|
netPerformancePercentage,
|
||||||
totalInvestment
|
totalInvestment
|
||||||
@ -479,25 +522,28 @@ export class PortfolioCalculator {
|
|||||||
} = {};
|
} = {};
|
||||||
if (j >= 0) {
|
if (j >= 0) {
|
||||||
const currencies: { [name: string]: Currency } = {};
|
const currencies: { [name: string]: Currency } = {};
|
||||||
const symbols: string[] = [];
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
|
|
||||||
for (const item of this.transactionPoints[j].items) {
|
for (const item of this.transactionPoints[j].items) {
|
||||||
currencies[item.symbol] = item.currency;
|
currencies[item.symbol] = item.currency;
|
||||||
symbols.push(item.symbol);
|
dataGatheringItems.push({
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
symbol: item.symbol
|
||||||
|
});
|
||||||
investment = investment.add(item.investment);
|
investment = investment.add(item.investment);
|
||||||
fees = fees.add(item.fee);
|
fees = fees.add(item.fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
let marketSymbols: GetValueObject[] = [];
|
let marketSymbols: GetValueObject[] = [];
|
||||||
if (symbols.length > 0) {
|
if (dataGatheringItems.length > 0) {
|
||||||
try {
|
try {
|
||||||
marketSymbols = await this.currentRateService.getValues({
|
marketSymbols = await this.currentRateService.getValues({
|
||||||
|
currencies,
|
||||||
|
dataGatheringItems,
|
||||||
dateQuery: {
|
dateQuery: {
|
||||||
gte: startDate,
|
gte: startDate,
|
||||||
lt: endOfDay(endDate)
|
lt: endOfDay(endDate)
|
||||||
},
|
},
|
||||||
symbols,
|
|
||||||
currencies,
|
|
||||||
userCurrency: this.currency
|
userCurrency: this.currency
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -223,6 +223,7 @@ export class PortfolioController {
|
|||||||
return nullifyValuesInObject(position, [
|
return nullifyValuesInObject(position, [
|
||||||
'grossPerformance',
|
'grossPerformance',
|
||||||
'investment',
|
'investment',
|
||||||
|
'netPerformance',
|
||||||
'quantity'
|
'quantity'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -246,6 +247,7 @@ export class PortfolioController {
|
|||||||
'cash',
|
'cash',
|
||||||
'committedFunds',
|
'committedFunds',
|
||||||
'currentGrossPerformance',
|
'currentGrossPerformance',
|
||||||
|
'currentNetPerformance',
|
||||||
'currentValue',
|
'currentValue',
|
||||||
'fees',
|
'fees',
|
||||||
'netWorth',
|
'netWorth',
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
|
differenceInDays,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
@ -58,7 +59,7 @@ import {
|
|||||||
subDays,
|
subDays,
|
||||||
subYears
|
subYears
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty, isNumber } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
@ -190,12 +191,18 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
|
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
|
||||||
|
|
||||||
|
const dataGatheringItems = currentPositions.positions.map((position) => {
|
||||||
|
return {
|
||||||
|
dataSource: position.dataSource,
|
||||||
|
symbol: position.symbol
|
||||||
|
};
|
||||||
|
});
|
||||||
const symbols = currentPositions.positions.map(
|
const symbols = currentPositions.positions.map(
|
||||||
(position) => position.symbol
|
(position) => position.symbol
|
||||||
);
|
);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.get(symbols),
|
this.dataProviderService.get(dataGatheringItems),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -296,6 +303,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
dataSource: order.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
fee: new Big(order.fee),
|
fee: new Big(order.fee),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
@ -325,6 +333,7 @@ export class PortfolioService {
|
|||||||
const {
|
const {
|
||||||
averagePrice,
|
averagePrice,
|
||||||
currency,
|
currency,
|
||||||
|
dataSource,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
quantity,
|
quantity,
|
||||||
@ -350,7 +359,7 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
[aSymbol],
|
[{ dataSource, symbol: aSymbol }],
|
||||||
'day',
|
'day',
|
||||||
parseISO(firstBuyDate),
|
parseISO(firstBuyDate),
|
||||||
new Date()
|
new Date()
|
||||||
@ -420,11 +429,13 @@ export class PortfolioService {
|
|||||||
symbol: aSymbol
|
symbol: aSymbol
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const currentData = await this.dataProviderService.get([aSymbol]);
|
const currentData = await this.dataProviderService.get([
|
||||||
|
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
||||||
|
]);
|
||||||
const marketPrice = currentData[aSymbol]?.marketPrice;
|
const marketPrice = currentData[aSymbol]?.marketPrice;
|
||||||
|
|
||||||
let historicalData = await this.dataProviderService.getHistorical(
|
let historicalData = await this.dataProviderService.getHistorical(
|
||||||
[aSymbol],
|
[{ dataSource: DataSource.YAHOO, symbol: aSymbol }],
|
||||||
'day',
|
'day',
|
||||||
portfolioStart,
|
portfolioStart,
|
||||||
new Date()
|
new Date()
|
||||||
@ -506,10 +517,16 @@ export class PortfolioService {
|
|||||||
const positions = currentPositions.positions.filter(
|
const positions = currentPositions.positions.filter(
|
||||||
(item) => !item.quantity.eq(0)
|
(item) => !item.quantity.eq(0)
|
||||||
);
|
);
|
||||||
|
const dataGatheringItem = positions.map((position) => {
|
||||||
|
return {
|
||||||
|
dataSource: position.dataSource,
|
||||||
|
symbol: position.symbol
|
||||||
|
};
|
||||||
|
});
|
||||||
const symbols = positions.map((position) => position.symbol);
|
const symbols = positions.map((position) => position.symbol);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.get(symbols),
|
this.dataProviderService.get(dataGatheringItem),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -557,6 +574,7 @@ export class PortfolioService {
|
|||||||
return {
|
return {
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
performance: {
|
performance: {
|
||||||
|
annualizedPerformancePercent: 0,
|
||||||
currentGrossPerformance: 0,
|
currentGrossPerformance: 0,
|
||||||
currentGrossPerformancePercent: 0,
|
currentGrossPerformancePercent: 0,
|
||||||
currentNetPerformance: 0,
|
currentNetPerformance: 0,
|
||||||
@ -575,6 +593,8 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hasErrors = currentPositions.hasErrors;
|
const hasErrors = currentPositions.hasErrors;
|
||||||
|
const annualizedPerformancePercent =
|
||||||
|
currentPositions.netAnnualizedPerformance.toNumber();
|
||||||
const currentValue = currentPositions.currentValue.toNumber();
|
const currentValue = currentPositions.currentValue.toNumber();
|
||||||
const currentGrossPerformance =
|
const currentGrossPerformance =
|
||||||
currentPositions.grossPerformance.toNumber();
|
currentPositions.grossPerformance.toNumber();
|
||||||
@ -587,6 +607,7 @@ export class PortfolioService {
|
|||||||
return {
|
return {
|
||||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||||
performance: {
|
performance: {
|
||||||
|
annualizedPerformancePercent,
|
||||||
currentGrossPerformance,
|
currentGrossPerformance,
|
||||||
currentGrossPerformancePercent,
|
currentGrossPerformancePercent,
|
||||||
currentNetPerformance,
|
currentNetPerformance,
|
||||||
@ -808,6 +829,7 @@ export class PortfolioService {
|
|||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
|
dataSource: order.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
fee: new Big(
|
fee: new Big(
|
||||||
this.exchangeRateDataService.toCurrency(
|
this.exchangeRateDataService.toCurrency(
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
@ -46,10 +47,20 @@ export class SymbolController {
|
|||||||
/**
|
/**
|
||||||
* Must be after /lookup
|
* Must be after /lookup
|
||||||
*/
|
*/
|
||||||
@Get(':symbol')
|
@Get(':dataSource/:symbol')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getPosition(@Param('symbol') symbol): Promise<SymbolItem> {
|
public async getSymbolData(
|
||||||
const result = await this.symbolService.get(symbol);
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<SymbolItem> {
|
||||||
|
if (!DataSource[dataSource]) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.symbolService.get({ dataSource, symbol });
|
||||||
|
|
||||||
if (!result || isEmpty(result)) {
|
if (!result || isEmpty(result)) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, DataSource } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
@ -13,15 +14,15 @@ export class SymbolService {
|
|||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(aSymbol: string): Promise<SymbolItem> {
|
public async get(dataGatheringItem: IDataGatheringItem): Promise<SymbolItem> {
|
||||||
const response = await this.dataProviderService.get([aSymbol]);
|
const response = await this.dataProviderService.get([dataGatheringItem]);
|
||||||
const { currency, dataSource, marketPrice } = response[aSymbol] ?? {};
|
const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {};
|
||||||
|
|
||||||
if (dataSource && marketPrice) {
|
if (dataGatheringItem.dataSource && marketPrice) {
|
||||||
return {
|
return {
|
||||||
dataSource,
|
|
||||||
marketPrice,
|
marketPrice,
|
||||||
currency: <Currency>(<unknown>currency)
|
currency: <Currency>(<unknown>currency),
|
||||||
|
dataSource: dataGatheringItem.dataSource
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,17 +3,11 @@ import {
|
|||||||
currencyPairs,
|
currencyPairs,
|
||||||
ghostfolioFearAndGreedIndexSymbol
|
ghostfolioFearAndGreedIndexSymbol
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import { DATE_FORMAT, getUtc, resetHours } from '@ghostfolio/common/helper';
|
||||||
DATE_FORMAT,
|
|
||||||
getUtc,
|
|
||||||
isGhostfolioScraperApiSymbol,
|
|
||||||
resetHours
|
|
||||||
} from '@ghostfolio/common/helper';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
differenceInHours,
|
differenceInHours,
|
||||||
endOfToday,
|
|
||||||
format,
|
format,
|
||||||
getDate,
|
getDate,
|
||||||
getMonth,
|
getMonth,
|
||||||
@ -123,20 +117,17 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async gatherProfileData(aSymbols?: string[]) {
|
public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) {
|
||||||
console.log('Profile data gathering has been started.');
|
console.log('Profile data gathering has been started.');
|
||||||
console.time('data-gathering-profile');
|
console.time('data-gathering-profile');
|
||||||
|
|
||||||
let symbols = aSymbols;
|
let dataGatheringItems = aDataGatheringItems;
|
||||||
|
|
||||||
if (!symbols) {
|
if (!dataGatheringItems) {
|
||||||
const dataGatheringItems = await this.getSymbolsProfileData();
|
dataGatheringItems = await this.getSymbolsProfileData();
|
||||||
symbols = dataGatheringItems.map((dataGatheringItem) => {
|
|
||||||
return dataGatheringItem.symbol;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentData = await this.dataProviderService.get(symbols);
|
const currentData = await this.dataProviderService.get(dataGatheringItems);
|
||||||
|
|
||||||
for (const [
|
for (const [
|
||||||
symbol,
|
symbol,
|
||||||
@ -215,6 +206,7 @@ export class DataGatheringService {
|
|||||||
try {
|
try {
|
||||||
await this.prismaService.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: currentDate,
|
date: currentDate,
|
||||||
marketPrice: lastMarketPrice
|
marketPrice: lastMarketPrice
|
||||||
@ -309,27 +301,20 @@ export class DataGatheringService {
|
|||||||
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = subDays(resetHours(new Date()), 7);
|
const startDate = subDays(resetHours(new Date()), 7);
|
||||||
|
|
||||||
const distinctOrders = await this.prismaService.order.findMany({
|
const symbolProfilesToGather = (
|
||||||
distinct: ['symbol'],
|
await this.prismaService.symbolProfile.findMany({
|
||||||
orderBy: [{ symbol: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: { dataSource: true, symbol: true },
|
select: {
|
||||||
where: {
|
dataSource: true,
|
||||||
date: {
|
symbol: true
|
||||||
lt: endOfToday() // no draft
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const distinctOrdersWithDate: IDataGatheringItem[] = distinctOrders
|
|
||||||
.filter((distinctOrder) => {
|
|
||||||
return !isGhostfolioScraperApiSymbol(distinctOrder.symbol);
|
|
||||||
})
|
})
|
||||||
.map((distinctOrder) => {
|
).map((symbolProfile) => {
|
||||||
return {
|
return {
|
||||||
...distinctOrder,
|
...symbolProfile,
|
||||||
date: startDate
|
date: startDate
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const currencyPairsToGather = currencyPairs.map(
|
const currencyPairsToGather = currencyPairs.map(
|
||||||
({ dataSource, symbol }) => {
|
({ dataSource, symbol }) => {
|
||||||
@ -348,7 +333,7 @@ export class DataGatheringService {
|
|||||||
...this.getBenchmarksToGather(startDate),
|
...this.getBenchmarksToGather(startDate),
|
||||||
...customSymbolsToGather,
|
...customSymbolsToGather,
|
||||||
...currencyPairsToGather,
|
...currencyPairsToGather,
|
||||||
...distinctOrdersWithDate
|
...symbolProfilesToGather
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,22 +353,20 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const distinctOrders = await this.prismaService.order.findMany({
|
const symbolProfilesToGather =
|
||||||
distinct: ['symbol'],
|
await this.prismaService.symbolProfile.findMany({
|
||||||
orderBy: [{ date: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: { dataSource: true, date: true, symbol: true },
|
select: {
|
||||||
where: {
|
dataSource: true,
|
||||||
date: {
|
symbol: true
|
||||||
lt: endOfToday() // no draft
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...this.getBenchmarksToGather(startDate),
|
...this.getBenchmarksToGather(startDate),
|
||||||
...customSymbolsToGather,
|
...customSymbolsToGather,
|
||||||
...currencyPairsToGather,
|
...currencyPairsToGather,
|
||||||
...distinctOrders
|
...symbolProfilesToGather
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,11 +6,7 @@ import {
|
|||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import {
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
DATE_FORMAT,
|
|
||||||
isGhostfolioScraperApiSymbol,
|
|
||||||
isRakutenRapidApiSymbol
|
|
||||||
} from '@ghostfolio/common/helper';
|
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
@ -20,8 +16,8 @@ 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 {
|
import {
|
||||||
convertToYahooFinanceSymbol,
|
YahooFinanceService,
|
||||||
YahooFinanceService
|
convertToYahooFinanceSymbol
|
||||||
} from './yahoo-finance/yahoo-finance.service';
|
} from './yahoo-finance/yahoo-finance.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -37,53 +33,32 @@ export class DataProviderService {
|
|||||||
this.rakutenRapidApiService?.setPrisma(this.prismaService);
|
this.rakutenRapidApiService?.setPrisma(this.prismaService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async get(items: IDataGatheringItem[]): Promise<{
|
||||||
aSymbols: string[]
|
[symbol: string]: IDataProviderResponse;
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}> {
|
||||||
if (aSymbols.length === 1) {
|
const response: {
|
||||||
const symbol = aSymbols[0];
|
[symbol: string]: IDataProviderResponse;
|
||||||
|
} = {};
|
||||||
|
|
||||||
if (isGhostfolioScraperApiSymbol(symbol)) {
|
for (const item of items) {
|
||||||
return this.ghostfolioScraperApiService.get(aSymbols);
|
if (item.dataSource === DataSource.ALPHA_VANTAGE) {
|
||||||
} else if (isRakutenRapidApiSymbol(symbol)) {
|
response[item.symbol] = (
|
||||||
return this.rakutenRapidApiService.get(aSymbols);
|
await this.alphaVantageService.get([item.symbol])
|
||||||
}
|
)[item.symbol];
|
||||||
}
|
} else if (item.dataSource === DataSource.GHOSTFOLIO) {
|
||||||
|
response[item.symbol] = (
|
||||||
const yahooFinanceSymbols = aSymbols
|
await this.ghostfolioScraperApiService.get([item.symbol])
|
||||||
.filter((symbol) => {
|
)[item.symbol];
|
||||||
return (
|
} else if (item.dataSource === DataSource.RAKUTEN) {
|
||||||
!isGhostfolioScraperApiSymbol(symbol) &&
|
response[item.symbol] = (
|
||||||
!isRakutenRapidApiSymbol(symbol)
|
await this.rakutenRapidApiService.get([item.symbol])
|
||||||
);
|
)[item.symbol];
|
||||||
})
|
} else if (item.dataSource === DataSource.YAHOO) {
|
||||||
.map((symbol) => {
|
response[item.symbol] = (
|
||||||
return convertToYahooFinanceSymbol(symbol);
|
await this.yahooFinanceService.get([
|
||||||
});
|
convertToYahooFinanceSymbol(item.symbol)
|
||||||
|
])
|
||||||
const response = await this.yahooFinanceService.get(yahooFinanceSymbols);
|
)[item.symbol];
|
||||||
|
|
||||||
const ghostfolioScraperApiSymbols = aSymbols.filter((symbol) => {
|
|
||||||
return isGhostfolioScraperApiSymbol(symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const symbol of ghostfolioScraperApiSymbols) {
|
|
||||||
if (symbol) {
|
|
||||||
const ghostfolioScraperApiResult =
|
|
||||||
await this.ghostfolioScraperApiService.get([symbol]);
|
|
||||||
response[symbol] = ghostfolioScraperApiResult[symbol];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rakutenRapidApiSymbols = aSymbols.filter((symbol) => {
|
|
||||||
return isRakutenRapidApiSymbol(symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const symbol of rakutenRapidApiSymbols) {
|
|
||||||
if (symbol) {
|
|
||||||
const rakutenRapidApiResult =
|
|
||||||
await this.ghostfolioScraperApiService.get([symbol]);
|
|
||||||
response[symbol] = rakutenRapidApiResult[symbol];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +66,7 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbols: string[],
|
aItems: IDataGatheringItem[],
|
||||||
aGranularity: Granularity = 'month',
|
aGranularity: Granularity = 'month',
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date
|
||||||
@ -115,8 +90,17 @@ export class DataProviderService {
|
|||||||
)}'`
|
)}'`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const dataSources = aItems.map((item) => {
|
||||||
|
return item.dataSource;
|
||||||
|
});
|
||||||
|
const symbols = aItems.map((item) => {
|
||||||
|
return item.symbol;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const queryRaw = `SELECT * FROM "MarketData" WHERE "symbol" IN ('${aSymbols.join(
|
const queryRaw = `SELECT * FROM "MarketData" WHERE "dataSource" IN ('${dataSources.join(
|
||||||
|
`','`
|
||||||
|
)}') AND "symbol" IN ('${symbols.join(
|
||||||
`','`
|
`','`
|
||||||
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
|
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
|
||||||
|
|
||||||
@ -175,13 +159,24 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||||
const { items } = await this.getDataProvider(
|
const promises: Promise<{ items: LookupItem[] }>[] = [];
|
||||||
<DataSource>this.configurationService.get('DATA_SOURCES')[0]
|
let lookupItems: LookupItem[] = [];
|
||||||
).search(aSymbol);
|
|
||||||
|
|
||||||
const filteredItems = items.filter((item) => {
|
for (const dataSource of this.configurationService.get('DATA_SOURCES')) {
|
||||||
|
promises.push(
|
||||||
|
this.getDataProvider(DataSource[dataSource]).search(aSymbol)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResults = await Promise.all(promises);
|
||||||
|
|
||||||
|
searchResults.forEach((searchResult) => {
|
||||||
|
lookupItems = lookupItems.concat(searchResult.items);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredItems = lookupItems.filter((lookupItem) => {
|
||||||
// Only allow symbols with supported currency
|
// Only allow symbols with supported currency
|
||||||
return item.currency ? true : false;
|
return lookupItem.currency ? true : false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -94,6 +94,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
await this.prismaService.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
|
dataSource: DataSource.RAKUTEN,
|
||||||
date: subWeeks(getToday(), 1),
|
date: subWeeks(getToday(), 1),
|
||||||
marketPrice: fgi.oneWeekAgo.value
|
marketPrice: fgi.oneWeekAgo.value
|
||||||
}
|
}
|
||||||
@ -102,6 +103,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
await this.prismaService.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
|
dataSource: DataSource.RAKUTEN,
|
||||||
date: subMonths(getToday(), 1),
|
date: subMonths(getToday(), 1),
|
||||||
marketPrice: fgi.oneMonthAgo.value
|
marketPrice: fgi.oneMonthAgo.value
|
||||||
}
|
}
|
||||||
@ -110,6 +112,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
await this.prismaService.marketData.create({
|
await this.prismaService.marketData.create({
|
||||||
data: {
|
data: {
|
||||||
symbol,
|
symbol,
|
||||||
|
dataSource: DataSource.RAKUTEN,
|
||||||
date: subYears(getToday(), 1),
|
date: subYears(getToday(), 1),
|
||||||
marketPrice: fgi.oneYearAgo.value
|
marketPrice: fgi.oneYearAgo.value
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { currencyPairs } from '@ghostfolio/common/config';
|
import { currencyPairs } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { isEmpty, isNumber } from 'lodash';
|
import { isEmpty, isNumber } from 'lodash';
|
||||||
|
|
||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExchangeRateDataService {
|
export class ExchangeRateDataService {
|
||||||
private currencyPairs: string[] = [];
|
private currencyPairs: IDataGatheringItem[] = [];
|
||||||
private exchangeRates: { [currencyPair: string]: number } = {};
|
private exchangeRates: { [currencyPair: string]: number } = {};
|
||||||
|
|
||||||
public constructor(private dataProviderService: DataProviderService) {
|
public constructor(private dataProviderService: DataProviderService) {
|
||||||
@ -20,8 +21,8 @@ export class ExchangeRateDataService {
|
|||||||
this.currencyPairs = [];
|
this.currencyPairs = [];
|
||||||
this.exchangeRates = {};
|
this.exchangeRates = {};
|
||||||
|
|
||||||
for (const { currency1, currency2 } of currencyPairs) {
|
for (const { currency1, currency2, dataSource } of currencyPairs) {
|
||||||
this.addCurrencyPairs(currency1, currency2);
|
this.addCurrencyPairs({ currency1, currency2, dataSource });
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadCurrencies();
|
await this.loadCurrencies();
|
||||||
@ -39,8 +40,8 @@ export class ExchangeRateDataService {
|
|||||||
// Load currencies directly from data provider as a fallback
|
// Load currencies directly from data provider as a fallback
|
||||||
// if historical data is not yet available
|
// if historical data is not yet available
|
||||||
const historicalData = await this.dataProviderService.get(
|
const historicalData = await this.dataProviderService.get(
|
||||||
this.currencyPairs.map((currencyPair) => {
|
this.currencyPairs.map(({ dataSource, symbol }) => {
|
||||||
return currencyPair;
|
return { dataSource, symbol };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -67,21 +68,21 @@ export class ExchangeRateDataService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currencyPairs.forEach((pair) => {
|
this.currencyPairs.forEach(({ symbol }) => {
|
||||||
const [currency1, currency2] = pair.match(/.{1,3}/g);
|
const [currency1, currency2] = symbol.match(/.{1,3}/g);
|
||||||
const date = format(getYesterday(), DATE_FORMAT);
|
const date = format(getYesterday(), DATE_FORMAT);
|
||||||
|
|
||||||
this.exchangeRates[pair] = resultExtended[pair]?.[date]?.marketPrice;
|
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
||||||
|
|
||||||
if (!this.exchangeRates[pair]) {
|
if (!this.exchangeRates[symbol]) {
|
||||||
// Not found, calculate indirectly via USD
|
// Not found, calculate indirectly via USD
|
||||||
this.exchangeRates[pair] =
|
this.exchangeRates[symbol] =
|
||||||
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
|
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
|
||||||
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
|
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
|
||||||
|
|
||||||
// Calculate the opposite direction
|
// Calculate the opposite direction
|
||||||
this.exchangeRates[`${currency2}${currency1}`] =
|
this.exchangeRates[`${currency2}${currency1}`] =
|
||||||
1 / this.exchangeRates[pair];
|
1 / this.exchangeRates[symbol];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -123,8 +124,22 @@ export class ExchangeRateDataService {
|
|||||||
return aValue;
|
return aValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addCurrencyPairs(aCurrency1: Currency, aCurrency2: Currency) {
|
private addCurrencyPairs({
|
||||||
this.currencyPairs.push(`${aCurrency1}${aCurrency2}`);
|
currency1,
|
||||||
this.currencyPairs.push(`${aCurrency2}${aCurrency1}`);
|
currency2,
|
||||||
|
dataSource
|
||||||
|
}: {
|
||||||
|
currency1: Currency;
|
||||||
|
currency2: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
|
}) {
|
||||||
|
this.currencyPairs.push({
|
||||||
|
dataSource,
|
||||||
|
symbol: `${currency1}${currency2}`
|
||||||
|
});
|
||||||
|
this.currencyPairs.push({
|
||||||
|
dataSource,
|
||||||
|
symbol: `${currency2}${currency1}`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@
|
|||||||
<div class="col"><hr /></div>
|
<div class="col"><hr /></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>Net Worth</div>
|
<div class="d-flex flex-grow-1 font-weight-bold" i18n>Net Worth</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"
|
||||||
@ -156,4 +156,17 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1 ml-3" i18n>Annualized Performance</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?.annualizedPerformancePercent"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +32,12 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you encounter a bug or would like to suggest an improvement or a
|
If you encounter a bug or would like to suggest an improvement or a
|
||||||
new feature, please tweet to
|
new feature, please join the Ghostfolio
|
||||||
|
<a
|
||||||
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
|
title="Join the Ghostfolio Slack channel"
|
||||||
|
>Slack channel</a
|
||||||
|
>, tweet to
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/ghostfolio_"
|
href="https://twitter.com/ghostfolio_"
|
||||||
title="Tweet to Ghostfolio on Twitter"
|
title="Tweet to Ghostfolio on Twitter"
|
||||||
@ -65,6 +70,14 @@
|
|||||||
>
|
>
|
||||||
<ion-icon name="mail" size="large"></ion-icon>
|
<ion-icon name="mail" size="large"></ion-icon>
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
class="mx-2"
|
||||||
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
|
mat-icon-button
|
||||||
|
title="Join the Ghostfolio Slack channel"
|
||||||
|
>
|
||||||
|
<ion-icon name="logo-slack" size="large"></ion-icon>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
class="mx-2"
|
class="mx-2"
|
||||||
href="https://github.com/ghostfolio/ghostfolio"
|
href="https://github.com/ghostfolio/ghostfolio"
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -112,7 +113,10 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchSymbolItem(ghostfolioFearAndGreedIndexSymbol)
|
.fetchSymbolItem({
|
||||||
|
dataSource: DataSource.RAKUTEN,
|
||||||
|
symbol: ghostfolioFearAndGreedIndexSymbol
|
||||||
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketPrice }) => {
|
.subscribe(({ marketPrice }) => {
|
||||||
this.fearAndGreedIndex = marketPrice;
|
this.fearAndGreedIndex = marketPrice;
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
OnDestroy
|
OnDestroy,
|
||||||
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl, Validators } from '@angular/forms';
|
import { FormControl, Validators } from '@angular/forms';
|
||||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
@ -11,6 +12,7 @@ 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 { isString } from 'lodash';
|
||||||
import { EMPTY, Observable, Subject } from 'rxjs';
|
import { EMPTY, Observable, Subject } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
@ -31,13 +33,18 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
|||||||
templateUrl: 'create-or-update-transaction-dialog.html'
|
templateUrl: 'create-or-update-transaction-dialog.html'
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
||||||
|
@ViewChild('autocomplete') autocomplete;
|
||||||
|
|
||||||
public currencies: Currency[] = [];
|
public currencies: Currency[] = [];
|
||||||
public currentMarketPrice = null;
|
public currentMarketPrice = null;
|
||||||
public filteredLookupItems: Observable<LookupItem[]>;
|
public filteredLookupItems: Observable<LookupItem[]>;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
public searchSymbolCtrl = new FormControl(
|
public searchSymbolCtrl = new FormControl(
|
||||||
this.data.transaction.symbol,
|
{
|
||||||
|
dataSource: this.data.transaction.dataSource,
|
||||||
|
name: this.data.transaction.symbol
|
||||||
|
},
|
||||||
Validators.required
|
Validators.required
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -60,9 +67,9 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
startWith(''),
|
startWith(''),
|
||||||
debounceTime(400),
|
debounceTime(400),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
switchMap((aQuery: string) => {
|
switchMap((query: string) => {
|
||||||
if (aQuery) {
|
if (isString(query)) {
|
||||||
return this.dataService.fetchSymbols(aQuery);
|
return this.dataService.fetchSymbols(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@ -71,7 +78,10 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
|
|
||||||
if (this.data.transaction.symbol) {
|
if (this.data.transaction.symbol) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchSymbolItem(this.data.transaction.symbol)
|
.fetchSymbolItem({
|
||||||
|
dataSource: this.data.transaction.dataSource,
|
||||||
|
symbol: this.data.transaction.symbol
|
||||||
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketPrice }) => {
|
.subscribe(({ marketPrice }) => {
|
||||||
this.currentMarketPrice = marketPrice;
|
this.currentMarketPrice = marketPrice;
|
||||||
@ -85,9 +95,21 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
this.data.transaction.unitPrice = this.currentMarketPrice;
|
this.data.transaction.unitPrice = this.currentMarketPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public displayFn(aLookupItem: LookupItem) {
|
||||||
|
return aLookupItem?.name ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
public onBlurSymbol() {
|
public onBlurSymbol() {
|
||||||
const symbol = this.searchSymbolCtrl.value;
|
this.data.transaction.currency = null;
|
||||||
this.updateSymbol(symbol);
|
this.data.transaction.dataSource = null;
|
||||||
|
|
||||||
|
if (this.autocomplete.isOpen) {
|
||||||
|
this.searchSymbolCtrl.setErrors({ incorrect: true });
|
||||||
|
} else {
|
||||||
|
this.data.transaction.unitPrice = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCancel(): void {
|
public onCancel(): void {
|
||||||
@ -95,7 +117,8 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
||||||
this.updateSymbol(event.option.value);
|
this.data.transaction.dataSource = event.option.value.dataSource;
|
||||||
|
this.updateSymbol(event.option.value.symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
@ -106,10 +129,15 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
private updateSymbol(symbol: string) {
|
private updateSymbol(symbol: string) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.searchSymbolCtrl.setErrors(null);
|
||||||
|
|
||||||
this.data.transaction.symbol = symbol;
|
this.data.transaction.symbol = symbol;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchSymbolItem(this.data.transaction.symbol)
|
.fetchSymbolItem({
|
||||||
|
dataSource: this.data.transaction.dataSource,
|
||||||
|
symbol: this.data.transaction.symbol
|
||||||
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
this.data.transaction.currency = null;
|
this.data.transaction.currency = null;
|
||||||
|
@ -28,18 +28,19 @@
|
|||||||
matInput
|
matInput
|
||||||
required
|
required
|
||||||
[formControl]="searchSymbolCtrl"
|
[formControl]="searchSymbolCtrl"
|
||||||
[matAutocomplete]="auto"
|
[matAutocomplete]="autocomplete"
|
||||||
(blur)="onBlurSymbol()"
|
(blur)="onBlurSymbol()"
|
||||||
/>
|
/>
|
||||||
<mat-autocomplete
|
<mat-autocomplete
|
||||||
#auto="matAutocomplete"
|
#autocomplete="matAutocomplete"
|
||||||
|
[displayWith]="displayFn"
|
||||||
(optionSelected)="onUpdateSymbol($event)"
|
(optionSelected)="onUpdateSymbol($event)"
|
||||||
>
|
>
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let lookupItem of filteredLookupItems | async"
|
*ngFor="let lookupItem of filteredLookupItems | async"
|
||||||
class="autocomplete"
|
class="autocomplete"
|
||||||
[value]="lookupItem.symbol"
|
[value]="lookupItem"
|
||||||
>
|
>
|
||||||
<span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span
|
<span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span
|
||||||
><span><b>{{ lookupItem.name }}</b></span>
|
><span><b>{{ lookupItem.name }}</b></span>
|
||||||
|
@ -2,8 +2,8 @@ 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/ui/logo';
|
|
||||||
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
|
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
|
||||||
|
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||||
|
|
||||||
import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
|
import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
|
||||||
|
|
||||||
|
@ -29,8 +29,11 @@ import {
|
|||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import {
|
||||||
import { Account as AccountModel } from '@prisma/client';
|
Account as AccountModel,
|
||||||
|
DataSource,
|
||||||
|
Order as OrderModel
|
||||||
|
} from '@prisma/client';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@ -108,8 +111,14 @@ export class DataService {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchSymbolItem(aSymbol: string) {
|
public fetchSymbolItem({
|
||||||
return this.http.get<SymbolItem>(`/api/symbol/${aSymbol}`);
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
return this.http.get<SymbolItem>(`/api/symbol/${dataSource}/${symbol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPositions({
|
public fetchPositions({
|
||||||
|
@ -6,7 +6,10 @@ import {
|
|||||||
PublicKeyCredentialRequestOptionsJSON
|
PublicKeyCredentialRequestOptionsJSON
|
||||||
} from '@ghostfolio/api/app/auth/interfaces/simplewebauthn';
|
} from '@ghostfolio/api/app/auth/interfaces/simplewebauthn';
|
||||||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { startAssertion, startAttestation } from '@simplewebauthn/browser';
|
import {
|
||||||
|
startAuthentication,
|
||||||
|
startRegistration
|
||||||
|
} from '@simplewebauthn/browser';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { catchError, switchMap, tap } from 'rxjs/operators';
|
import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -32,7 +35,7 @@ export class WebAuthnService {
|
|||||||
public register() {
|
public register() {
|
||||||
return this.http
|
return this.http
|
||||||
.get<PublicKeyCredentialCreationOptionsJSON>(
|
.get<PublicKeyCredentialCreationOptionsJSON>(
|
||||||
`/api/auth/webauthn/generate-attestation-options`,
|
`/api/auth/webauthn/generate-registration-options`,
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -41,7 +44,7 @@ export class WebAuthnService {
|
|||||||
return of(null);
|
return of(null);
|
||||||
}),
|
}),
|
||||||
switchMap((attOps) => {
|
switchMap((attOps) => {
|
||||||
return startAttestation(attOps);
|
return startRegistration(attOps);
|
||||||
}),
|
}),
|
||||||
switchMap((attResp) => {
|
switchMap((attResp) => {
|
||||||
return this.http.post<AuthDeviceDto>(
|
return this.http.post<AuthDeviceDto>(
|
||||||
@ -83,7 +86,7 @@ export class WebAuthnService {
|
|||||||
{ deviceId }
|
{ deviceId }
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(startAssertion),
|
switchMap(startAuthentication),
|
||||||
switchMap((assertionResponse) => {
|
switchMap((assertionResponse) => {
|
||||||
return this.http.post<{ authToken: string }>(
|
return this.http.post<{ authToken: string }>(
|
||||||
`/api/auth/webauthn/verify-assertion`,
|
`/api/auth/webauthn/verify-assertion`,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export interface PortfolioPerformance {
|
export interface PortfolioPerformance {
|
||||||
|
annualizedPerformancePercent: number;
|
||||||
currentGrossPerformance: number;
|
currentGrossPerformance: number;
|
||||||
currentGrossPerformancePercent: number;
|
currentGrossPerformancePercent: number;
|
||||||
currentNetPerformance: number;
|
currentNetPerformance: number;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { PortfolioPerformance } from './portfolio-performance.interface';
|
import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||||
|
|
||||||
export interface PortfolioSummary extends PortfolioPerformance {
|
export interface PortfolioSummary extends PortfolioPerformance {
|
||||||
|
annualizedPerformancePercent: number;
|
||||||
cash: number;
|
cash: number;
|
||||||
committedFunds: number;
|
committedFunds: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface TimelinePosition {
|
export interface TimelinePosition {
|
||||||
averagePrice: Big;
|
averagePrice: Big;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
|
@ -4,8 +4,10 @@
|
|||||||
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
|
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
|
||||||
>
|
>
|
||||||
<ng-container *ngIf="isNumber || value === null">
|
<ng-container *ngIf="isNumber || value === null">
|
||||||
<div *ngIf="colorizeSign && value > 0" class="mr-1 text-success">+</div>
|
<ng-container *ngIf="colorizeSign && !useAbsoluteValue">
|
||||||
<div *ngIf="colorizeSign && value < 0" class="mr-1 text-danger">-</div>
|
<div *ngIf="value > 0" class="mr-1 text-success">+</div>
|
||||||
|
<div *ngIf="value < 0" class="mr-1 text-danger">-</div>
|
||||||
|
</ng-container>
|
||||||
<div *ngIf="isPercent" [ngClass]="size === 'medium' ? 'h4 mb-0' : ''">
|
<div *ngIf="isPercent" [ngClass]="size === 'medium' ? 'h4 mb-0' : ''">
|
||||||
{{ formattedValue }}%
|
{{ formattedValue }}%
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,6 +36,33 @@ Label.args = {
|
|||||||
value: 7.25
|
value: 7.25
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PerformancePositive = Template.bind({});
|
||||||
|
PerformancePositive.args = {
|
||||||
|
locale: 'en-US',
|
||||||
|
colorizeSign: true,
|
||||||
|
isPercent: true,
|
||||||
|
value: 0.0136810853673890378
|
||||||
|
};
|
||||||
|
PerformancePositive.storyName = 'Performance (positive)';
|
||||||
|
|
||||||
|
export const PerformanceNegative = Template.bind({});
|
||||||
|
PerformanceNegative.args = {
|
||||||
|
locale: 'en-US',
|
||||||
|
colorizeSign: true,
|
||||||
|
isPercent: true,
|
||||||
|
value: -0.0136810853673890378
|
||||||
|
};
|
||||||
|
PerformanceNegative.storyName = 'Performance (negative)';
|
||||||
|
|
||||||
|
export const PerformanceCloseToZero = Template.bind({});
|
||||||
|
PerformanceCloseToZero.args = {
|
||||||
|
locale: 'en-US',
|
||||||
|
colorizeSign: true,
|
||||||
|
isPercent: true,
|
||||||
|
value: -2.388915360475e-8
|
||||||
|
};
|
||||||
|
PerformanceCloseToZero.storyName = 'Performance (negative zero)';
|
||||||
|
|
||||||
export const Precision = Template.bind({});
|
export const Precision = Template.bind({});
|
||||||
Precision.args = {
|
Precision.args = {
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
|
@ -43,7 +43,6 @@ export class ValueComponent implements OnChanges {
|
|||||||
this.absoluteValue = Math.abs(<number>this.value);
|
this.absoluteValue = Math.abs(<number>this.value);
|
||||||
|
|
||||||
if (this.colorizeSign) {
|
if (this.colorizeSign) {
|
||||||
this.useAbsoluteValue = true;
|
|
||||||
if (this.currency || this.isCurrency) {
|
if (this.currency || this.isCurrency) {
|
||||||
try {
|
try {
|
||||||
this.formattedValue = this.absoluteValue.toLocaleString(
|
this.formattedValue = this.absoluteValue.toLocaleString(
|
||||||
@ -106,5 +105,9 @@ export class ValueComponent implements OnChanges {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.formattedValue === '0.00') {
|
||||||
|
this.useAbsoluteValue = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.51.0",
|
"version": "1.54.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -68,9 +68,9 @@
|
|||||||
"@nestjs/serve-static": "2.1.4",
|
"@nestjs/serve-static": "2.1.4",
|
||||||
"@nrwl/angular": "12.8.0",
|
"@nrwl/angular": "12.8.0",
|
||||||
"@prisma/client": "2.30.2",
|
"@prisma/client": "2.30.2",
|
||||||
"@simplewebauthn/browser": "3.0.0",
|
"@simplewebauthn/browser": "4.1.0",
|
||||||
"@simplewebauthn/server": "3.0.0",
|
"@simplewebauthn/server": "4.1.0",
|
||||||
"@simplewebauthn/typescript-types": "3.0.0",
|
"@simplewebauthn/typescript-types": "4.0.0",
|
||||||
"@stripe/stripe-js": "1.15.0",
|
"@stripe/stripe-js": "1.15.0",
|
||||||
"alphavantage": "2.2.0",
|
"alphavantage": "2.2.0",
|
||||||
"angular-material-css-vars": "2.1.2",
|
"angular-material-css-vars": "2.1.2",
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Order" ALTER COLUMN "currency" DROP NOT NULL;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "MarketData" ADD COLUMN "dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO';
|
@ -61,9 +61,10 @@ model AuthDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model MarketData {
|
model MarketData {
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
dataSource DataSource @default(YAHOO)
|
||||||
date DateTime
|
date DateTime
|
||||||
id String @default(uuid())
|
id String @default(uuid())
|
||||||
symbol String
|
symbol String
|
||||||
marketPrice Float
|
marketPrice Float
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ model Order {
|
|||||||
accountId String?
|
accountId String?
|
||||||
accountUserId String?
|
accountUserId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
currency Currency
|
currency Currency?
|
||||||
dataSource DataSource @default(YAHOO)
|
dataSource DataSource @default(YAHOO)
|
||||||
date DateTime
|
date DateTime
|
||||||
fee Float
|
fee Float
|
||||||
|
42
yarn.lock
42
yarn.lock
@ -2519,7 +2519,7 @@
|
|||||||
consola "^2.15.0"
|
consola "^2.15.0"
|
||||||
node-fetch "^2.6.1"
|
node-fetch "^2.6.1"
|
||||||
|
|
||||||
"@peculiar/asn1-android@^2.0.26":
|
"@peculiar/asn1-android@^2.0.38":
|
||||||
version "2.0.38"
|
version "2.0.38"
|
||||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-android/-/asn1-android-2.0.38.tgz#193281f5a232e323d6f2c069c7a8e8e8f4a994bd"
|
resolved "https://registry.yarnpkg.com/@peculiar/asn1-android/-/asn1-android-2.0.38.tgz#193281f5a232e323d6f2c069c7a8e8e8f4a994bd"
|
||||||
integrity sha512-krWyggV6FgYf3fEPKVNjHVecLcQWlAu3/YhOyN+/L43dNKcsmqiEvuhqplh3aiXF62Ds0pqzqttWmdvoVqmSVQ==
|
integrity sha512-krWyggV6FgYf3fEPKVNjHVecLcQWlAu3/YhOyN+/L43dNKcsmqiEvuhqplh3aiXF62Ds0pqzqttWmdvoVqmSVQ==
|
||||||
@ -2528,7 +2528,7 @@
|
|||||||
asn1js "^2.1.1"
|
asn1js "^2.1.1"
|
||||||
tslib "^2.3.0"
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@peculiar/asn1-schema@^2.0.26", "@peculiar/asn1-schema@^2.0.38":
|
"@peculiar/asn1-schema@^2.0.38":
|
||||||
version "2.0.38"
|
version "2.0.38"
|
||||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.38.tgz#98b6f12daad275ecd6774dfe31fb62f362900412"
|
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.38.tgz#98b6f12daad275ecd6774dfe31fb62f362900412"
|
||||||
integrity sha512-zZ64UpCTm9me15nuCpPgJghSdbEm8atcDQPCyK+bKXjZAQ1735NCZXCSCfbckbQ4MH36Rm9403n/qMq77LFDzQ==
|
integrity sha512-zZ64UpCTm9me15nuCpPgJghSdbEm8atcDQPCyK+bKXjZAQ1735NCZXCSCfbckbQ4MH36Rm9403n/qMq77LFDzQ==
|
||||||
@ -2538,7 +2538,7 @@
|
|||||||
pvtsutils "^1.2.0"
|
pvtsutils "^1.2.0"
|
||||||
tslib "^2.3.0"
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@peculiar/asn1-x509@^2.0.26":
|
"@peculiar/asn1-x509@^2.0.38":
|
||||||
version "2.0.38"
|
version "2.0.38"
|
||||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.0.38.tgz#7ff3b5478d9c3784f0eb2fbe7693509da9de0a43"
|
resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.0.38.tgz#7ff3b5478d9c3784f0eb2fbe7693509da9de0a43"
|
||||||
integrity sha512-10aK9fSxlc1DK9nEcwh+WPFNhAheXSE9RbI5MyS7FdBhgq+Mz4Z9JqFfaBZm1Qp+5mPtUMOP6cXVo7aaYlgq7A==
|
integrity sha512-10aK9fSxlc1DK9nEcwh+WPFNhAheXSE9RbI5MyS7FdBhgq+Mz4Z9JqFfaBZm1Qp+5mPtUMOP6cXVo7aaYlgq7A==
|
||||||
@ -2613,32 +2613,32 @@
|
|||||||
"@angular-devkit/schematics" "12.1.4"
|
"@angular-devkit/schematics" "12.1.4"
|
||||||
jsonc-parser "3.0.0"
|
jsonc-parser "3.0.0"
|
||||||
|
|
||||||
"@simplewebauthn/browser@3.0.0":
|
"@simplewebauthn/browser@4.1.0":
|
||||||
version "3.0.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-3.0.0.tgz#3d76b199c9f474408a7ed75d86004423dd6ae38a"
|
resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-4.1.0.tgz#3e7fd66729405d6a2a2a187c93577b90a8e41786"
|
||||||
integrity sha512-P661gZX/QW0Rg2NRAMtW84Q3u4nhXkPef9LLU4btLJFYoXO8RBFfxcmyqwyf2QEb4B7+lFdp5EWfZV5T7FvuHw==
|
integrity sha512-tIsEfShC1rrqrsNb44tOFuSriAFCz4tkdDnCjHfn2rYxgz+t+yqEvuIRfJHQpFrWSnZPdsjrAHtasj6lzfGI6w==
|
||||||
|
|
||||||
"@simplewebauthn/server@3.0.0":
|
"@simplewebauthn/server@4.1.0":
|
||||||
version "3.0.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/server/-/server-3.0.0.tgz#eb1a5bbe2ecdda54363b178f4bb3e134f25641f0"
|
resolved "https://registry.yarnpkg.com/@simplewebauthn/server/-/server-4.1.0.tgz#9ad2e32cffa83833ff8a633775b2ace5e6926fa0"
|
||||||
integrity sha512-ymGX2obBrhY9R3OxrpCYaNGAovFHmMlQrGoNdVOe2R2JUBXC1Rg5JEUl1lGyaRykN1SyZqLgz86wAjDVuRITTA==
|
integrity sha512-52X5/U+5Fo0XYG1TuBBGgG0ap9c0ffpeq0GZfFio/DZDW4He0Arb7Q/XkHw96JK0X1sfRKNmnfC+NImplvIimA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@peculiar/asn1-android" "^2.0.26"
|
"@peculiar/asn1-android" "^2.0.38"
|
||||||
"@peculiar/asn1-schema" "^2.0.26"
|
"@peculiar/asn1-schema" "^2.0.38"
|
||||||
"@peculiar/asn1-x509" "^2.0.26"
|
"@peculiar/asn1-x509" "^2.0.38"
|
||||||
"@simplewebauthn/typescript-types" "^3.0.0"
|
"@simplewebauthn/typescript-types" "^4.0.0"
|
||||||
base64url "^3.0.1"
|
base64url "^3.0.1"
|
||||||
cbor "^5.1.0"
|
cbor "^5.1.0"
|
||||||
elliptic "^6.5.3"
|
elliptic "^6.5.3"
|
||||||
jsrsasign "^10.2.0"
|
jsrsasign "^10.4.0"
|
||||||
jwk-to-pem "^2.0.4"
|
jwk-to-pem "^2.0.4"
|
||||||
node-fetch "^2.6.0"
|
node-fetch "^2.6.0"
|
||||||
node-rsa "^1.1.1"
|
node-rsa "^1.1.1"
|
||||||
|
|
||||||
"@simplewebauthn/typescript-types@3.0.0", "@simplewebauthn/typescript-types@^3.0.0":
|
"@simplewebauthn/typescript-types@4.0.0", "@simplewebauthn/typescript-types@^4.0.0":
|
||||||
version "3.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-3.0.0.tgz#6712e9619d860f54f571cd27dbe167b2d9e5ab87"
|
resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-4.0.0.tgz#46ae4e69cb07305c57093a3ed99555437dfe0d49"
|
||||||
integrity sha512-bsk3EQWzPOZwP9C+ETVhcFDpZywY5sTqmNuGkNm3aNpc9Xh/mqZjy8nL0Sm7xwrlhY0zWAlOaIWQ3LvN5SoFhg==
|
integrity sha512-jqQ0bCeBO96CytB397vSrQ8ipozQzAmI57izA7izyglyu35JBV90I7+75fSX+ZGNHmMwDNnA3EGYtBLOIpkJEg==
|
||||||
|
|
||||||
"@sinonjs/commons@^1.7.0":
|
"@sinonjs/commons@^1.7.0":
|
||||||
version "1.8.3"
|
version "1.8.3"
|
||||||
@ -11343,7 +11343,7 @@ jsprim@^1.2.2:
|
|||||||
json-schema "0.2.3"
|
json-schema "0.2.3"
|
||||||
verror "1.10.0"
|
verror "1.10.0"
|
||||||
|
|
||||||
jsrsasign@^10.2.0:
|
jsrsasign@^10.4.0:
|
||||||
version "10.4.0"
|
version "10.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.4.0.tgz#362cc787079c03a363a958c03eb68d8545ba92f7"
|
resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.4.0.tgz#362cc787079c03a363a958c03eb68d8545ba92f7"
|
||||||
integrity sha512-C8qLhiAssh/b74KJpGhWuFGG9cFhJqMCVuuHXRibb3Z5vPuAW0ue0jUirpoExCdpdhv4nD3sZ1DAwJURYJTm9g==
|
integrity sha512-C8qLhiAssh/b74KJpGhWuFGG9cFhJqMCVuuHXRibb3Z5vPuAW0ue0jUirpoExCdpdhv4nD3sZ1DAwJURYJTm9g==
|
||||||
|
Reference in New Issue
Block a user