Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
695c378b48 | |||
fe975945d1 | |||
d8782b0d4c | |||
e14f08a8fb | |||
72c065a59d | |||
98dac4052a | |||
2083d28d02 |
18
CHANGELOG.md
18
CHANGELOG.md
@ -5,6 +5,24 @@ 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.43.0 - 24.08.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the data management of symbol profile data by countries (automated for stocks)
|
||||||
|
- Added a fallback for initially loading currencies if historical data is not yet available
|
||||||
|
|
||||||
|
## 1.42.0 - 22.08.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the subscription type to the users table of the admin control panel
|
||||||
|
- Introduced the sub classification of assets
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:push`)
|
||||||
|
|
||||||
## 1.41.0 - 21.08.2021
|
## 1.41.0 - 21.08.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
@ -14,9 +15,11 @@ import { AdminService } from './admin.service';
|
|||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
PrismaModule
|
PrismaModule,
|
||||||
|
SubscriptionModule
|
||||||
],
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [AdminService]
|
providers: [AdminService],
|
||||||
|
exports: [AdminService]
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
@ -9,9 +11,11 @@ import { differenceInDays } from 'date-fns';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly subscriptionService: SubscriptionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(): Promise<AdminData> {
|
public async get(): Promise<AdminData> {
|
||||||
@ -107,7 +111,8 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
id: true
|
id: true,
|
||||||
|
Subscription: true
|
||||||
},
|
},
|
||||||
take: 30,
|
take: 30,
|
||||||
where: {
|
where: {
|
||||||
@ -118,16 +123,23 @@ export class AdminService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return usersWithAnalytics.map(
|
return usersWithAnalytics.map(
|
||||||
({ _count, alias, Analytics, createdAt, id }) => {
|
({ _count, alias, Analytics, createdAt, id, Subscription }) => {
|
||||||
const daysSinceRegistration =
|
const daysSinceRegistration =
|
||||||
differenceInDays(new Date(), createdAt) + 1;
|
differenceInDays(new Date(), createdAt) + 1;
|
||||||
const engagement = Analytics.activityCount / daysSinceRegistration;
|
const engagement = Analytics.activityCount / daysSinceRegistration;
|
||||||
|
|
||||||
|
const subscription = this.configurationService.get(
|
||||||
|
'ENABLE_FEATURE_SUBSCRIPTION'
|
||||||
|
)
|
||||||
|
? this.subscriptionService.getSubscription(Subscription)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alias,
|
alias,
|
||||||
createdAt,
|
createdAt,
|
||||||
engagement,
|
engagement,
|
||||||
id,
|
id,
|
||||||
|
subscription,
|
||||||
accountCount: _count.Account || 0,
|
accountCount: _count.Account || 0,
|
||||||
lastActivity: Analytics.updatedAt,
|
lastActivity: Analytics.updatedAt,
|
||||||
transactionCount: _count.Order || 0
|
transactionCount: _count.Order || 0
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||||
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
|
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
|
||||||
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
@ -17,7 +18,8 @@ import { JwtStrategy } from './jwt.strategy';
|
|||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_SECRET_KEY,
|
secret: process.env.JWT_SECRET_KEY,
|
||||||
signOptions: { expiresIn: '180 days' }
|
signOptions: { expiresIn: '180 days' }
|
||||||
})
|
}),
|
||||||
|
SubscriptionModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AuthDeviceService,
|
AuthDeviceService,
|
||||||
|
@ -218,6 +218,7 @@ export class PortfolioService {
|
|||||||
allocationCurrent: value.div(totalValue).toNumber(),
|
allocationCurrent: value.div(totalValue).toNumber(),
|
||||||
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
||||||
assetClass: symbolProfile.assetClass,
|
assetClass: symbolProfile.assetClass,
|
||||||
|
assetSubClass: symbolProfile.assetSubClass,
|
||||||
countries: symbolProfile.countries,
|
countries: symbolProfile.countries,
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
exchange: dataProviderResponse.exchange,
|
exchange: dataProviderResponse.exchange,
|
||||||
|
@ -8,6 +8,7 @@ import { SubscriptionService } from './subscription.service';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
controllers: [SubscriptionController],
|
controllers: [SubscriptionController],
|
||||||
providers: [ConfigurationService, PrismaService, SubscriptionService]
|
providers: [ConfigurationService, PrismaService, SubscriptionService],
|
||||||
|
exports: [SubscriptionService]
|
||||||
})
|
})
|
||||||
export class SubscriptionModule {}
|
export class SubscriptionModule {}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { addDays } from 'date-fns';
|
import { Subscription } from '@prisma/client';
|
||||||
|
import { addDays, isBefore } from 'date-fns';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -86,4 +88,23 @@ export class SubscriptionService {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSubscription(aSubscriptions: Subscription[]) {
|
||||||
|
if (aSubscriptions.length > 0) {
|
||||||
|
const latestSubscription = aSubscriptions.reduce((a, b) => {
|
||||||
|
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
expiresAt: latestSubscription.expiresAt,
|
||||||
|
type: isBefore(new Date(), latestSubscription.expiresAt)
|
||||||
|
? SubscriptionType.Premium
|
||||||
|
: SubscriptionType.Basic
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: SubscriptionType.Basic
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
@ -11,7 +12,8 @@ import { UserService } from './user.service';
|
|||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_SECRET_KEY,
|
secret: process.env.JWT_SECRET_KEY,
|
||||||
signOptions: { expiresIn: '30 days' }
|
signOptions: { expiresIn: '30 days' }
|
||||||
})
|
}),
|
||||||
|
SubscriptionModule
|
||||||
],
|
],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [ConfigurationService, PrismaService, UserService],
|
providers: [ConfigurationService, PrismaService, UserService],
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { locale } from '@ghostfolio/common/config';
|
import { locale } from '@ghostfolio/common/config';
|
||||||
@ -6,7 +7,6 @@ import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
|||||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
||||||
import { isBefore } from 'date-fns';
|
|
||||||
|
|
||||||
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
||||||
import { UserSettings } from './interfaces/user-settings.interface';
|
import { UserSettings } from './interfaces/user-settings.interface';
|
||||||
@ -19,7 +19,8 @@ export class UserService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly subscriptionService: SubscriptionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getUser({
|
public async getUser({
|
||||||
@ -98,24 +99,9 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
if (userFromDatabase?.Subscription?.length > 0) {
|
user.subscription = this.subscriptionService.getSubscription(
|
||||||
const latestSubscription = userFromDatabase.Subscription.reduce(
|
userFromDatabase?.Subscription
|
||||||
(a, b) => {
|
);
|
||||||
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
user.subscription = {
|
|
||||||
expiresAt: latestSubscription.expiresAt,
|
|
||||||
type: isBefore(new Date(), latestSubscription.expiresAt)
|
|
||||||
? SubscriptionType.Premium
|
|
||||||
: SubscriptionType.Basic
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
user.subscription = {
|
|
||||||
type: SubscriptionType.Basic
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.subscription.type === SubscriptionType.Basic) {
|
if (user.subscription.type === SubscriptionType.Basic) {
|
||||||
user.permissions = user.permissions.filter((permission) => {
|
user.permissions = user.permissions.filter((permission) => {
|
||||||
|
@ -38,7 +38,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
if (isDataGatheringNeeded) {
|
if (isDataGatheringNeeded) {
|
||||||
console.log('7d data gathering has been started.');
|
console.log('7d data gathering has been started.');
|
||||||
console.time('7d-data-gathering');
|
console.time('data-gathering-7d');
|
||||||
|
|
||||||
await this.prismaService.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
@ -71,7 +71,7 @@ export class DataGatheringService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('7d data gathering has been completed.');
|
console.log('7d data gathering has been completed.');
|
||||||
console.timeEnd('7d-data-gathering');
|
console.timeEnd('data-gathering-7d');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
if (!isDataGatheringLocked) {
|
if (!isDataGatheringLocked) {
|
||||||
console.log('Max data gathering has been started.');
|
console.log('Max data gathering has been started.');
|
||||||
console.time('max-data-gathering');
|
console.time('data-gathering-max');
|
||||||
|
|
||||||
await this.prismaService.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
@ -115,13 +115,13 @@ export class DataGatheringService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('Max data gathering has been completed.');
|
console.log('Max data gathering has been completed.');
|
||||||
console.timeEnd('max-data-gathering');
|
console.timeEnd('data-gathering-max');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async gatherProfileData(aSymbols?: string[]) {
|
public async gatherProfileData(aSymbols?: string[]) {
|
||||||
console.log('Profile data gathering has been started.');
|
console.log('Profile data gathering has been started.');
|
||||||
console.time('profile-data-gathering');
|
console.time('data-gathering-profile');
|
||||||
|
|
||||||
let symbols = aSymbols;
|
let symbols = aSymbols;
|
||||||
|
|
||||||
@ -136,12 +136,14 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
for (const [
|
for (const [
|
||||||
symbol,
|
symbol,
|
||||||
{ assetClass, currency, dataSource, name }
|
{ assetClass, assetSubClass, countries, currency, dataSource, name }
|
||||||
] of Object.entries(currentData)) {
|
] of Object.entries(currentData)) {
|
||||||
try {
|
try {
|
||||||
await this.prismaService.symbolProfile.upsert({
|
await this.prismaService.symbolProfile.upsert({
|
||||||
create: {
|
create: {
|
||||||
assetClass,
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
name,
|
name,
|
||||||
@ -149,6 +151,8 @@ export class DataGatheringService {
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
assetClass,
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
countries,
|
||||||
currency,
|
currency,
|
||||||
name
|
name
|
||||||
},
|
},
|
||||||
@ -165,7 +169,7 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Profile data gathering has been completed.');
|
console.log('Profile data gathering has been completed.');
|
||||||
console.timeEnd('profile-data-gathering');
|
console.timeEnd('data-gathering-profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
||||||
|
@ -25,6 +25,7 @@ export interface IYahooFinancePrice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IYahooFinanceSummaryProfile {
|
export interface IYahooFinanceSummaryProfile {
|
||||||
|
country?: string;
|
||||||
industry?: string;
|
industry?: string;
|
||||||
sector?: string;
|
sector?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
|
@ -8,9 +8,15 @@ import {
|
|||||||
} from '@ghostfolio/common/helper';
|
} 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 { AssetClass, Currency, DataSource } from '@prisma/client';
|
import {
|
||||||
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
|
Currency,
|
||||||
|
DataSource
|
||||||
|
} from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
import { countries } from 'countries-list';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import * as yahooFinance from 'yahoo-finance';
|
import * as yahooFinance from 'yahoo-finance';
|
||||||
|
|
||||||
@ -22,6 +28,7 @@ import {
|
|||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
import {
|
import {
|
||||||
IYahooFinanceHistoricalResponse,
|
IYahooFinanceHistoricalResponse,
|
||||||
|
IYahooFinancePrice,
|
||||||
IYahooFinanceQuoteResponse
|
IYahooFinanceQuoteResponse
|
||||||
} from './interfaces/interfaces';
|
} from './interfaces/interfaces';
|
||||||
|
|
||||||
@ -60,8 +67,11 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
const symbol = convertFromYahooSymbol(yahooSymbol);
|
const symbol = convertFromYahooSymbol(yahooSymbol);
|
||||||
|
|
||||||
|
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
|
||||||
|
|
||||||
response[symbol] = {
|
response[symbol] = {
|
||||||
assetClass: this.parseAssetClass(value.price?.quoteType),
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
currency: parseCurrency(value.price?.currency),
|
currency: parseCurrency(value.price?.currency),
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: DataSource.YAHOO,
|
||||||
exchange: this.parseExchange(value.price?.exchangeName),
|
exchange: this.parseExchange(value.price?.exchangeName),
|
||||||
@ -83,6 +93,23 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
.toNumber();
|
.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add country if stock and available
|
||||||
|
if (
|
||||||
|
assetSubClass === AssetSubClass.STOCK &&
|
||||||
|
value.summaryProfile?.country
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const [code] = Object.entries(countries).find(([, country]) => {
|
||||||
|
return country.name === value.summaryProfile?.country;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
response[symbol].countries = [{ code, weight: 1 }];
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add url if available
|
||||||
const url = value.summaryProfile?.website;
|
const url = value.summaryProfile?.website;
|
||||||
if (url) {
|
if (url) {
|
||||||
response[symbol].url = url;
|
response[symbol].url = url;
|
||||||
@ -229,20 +256,29 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return aSymbol;
|
return aSymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAssetClass(aString: string): AssetClass {
|
private parseAssetClass(aPrice: IYahooFinancePrice): {
|
||||||
|
assetClass: AssetClass;
|
||||||
|
assetSubClass: AssetSubClass;
|
||||||
|
} {
|
||||||
let assetClass: AssetClass;
|
let assetClass: AssetClass;
|
||||||
|
let assetSubClass: AssetSubClass;
|
||||||
|
|
||||||
switch (aString?.toLowerCase()) {
|
switch (aPrice?.quoteType?.toLowerCase()) {
|
||||||
case 'cryptocurrency':
|
case 'cryptocurrency':
|
||||||
assetClass = AssetClass.CASH;
|
assetClass = AssetClass.CASH;
|
||||||
|
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||||
break;
|
break;
|
||||||
case 'equity':
|
case 'equity':
|
||||||
|
assetClass = AssetClass.EQUITY;
|
||||||
|
assetSubClass = AssetSubClass.STOCK;
|
||||||
|
break;
|
||||||
case 'etf':
|
case 'etf':
|
||||||
assetClass = AssetClass.EQUITY;
|
assetClass = AssetClass.EQUITY;
|
||||||
|
assetSubClass = AssetSubClass.ETF;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return assetClass;
|
return { assetClass, assetSubClass };
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseExchange(aString: string): string {
|
private parseExchange(aString: string): string {
|
||||||
|
@ -3,7 +3,7 @@ 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 } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { isNumber } from 'lodash';
|
import { isEmpty, isNumber } from 'lodash';
|
||||||
|
|
||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
|
|
||||||
@ -35,6 +35,24 @@ export class ExchangeRateDataService {
|
|||||||
getYesterday()
|
getYesterday()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isEmpty(result)) {
|
||||||
|
// Load currencies directly from data provider as a fallback
|
||||||
|
// if historical data is not yet available
|
||||||
|
const historicalData = await this.dataProviderService.get(
|
||||||
|
this.currencyPairs.map((currencyPair) => {
|
||||||
|
return currencyPair;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.keys(historicalData).forEach((key) => {
|
||||||
|
result[key] = {
|
||||||
|
[format(getYesterday(), DATE_FORMAT)]: {
|
||||||
|
marketPrice: historicalData[key].marketPrice
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const resultExtended = result;
|
const resultExtended = result;
|
||||||
|
|
||||||
Object.keys(result).forEach((pair) => {
|
Object.keys(result).forEach((pair) => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
AssetClass,
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
Currency,
|
Currency,
|
||||||
DataSource,
|
DataSource,
|
||||||
SymbolProfile
|
SymbolProfile
|
||||||
@ -35,6 +36,8 @@ export interface IDataProviderHistoricalResponse {
|
|||||||
|
|
||||||
export interface IDataProviderResponse {
|
export interface IDataProviderResponse {
|
||||||
assetClass?: AssetClass;
|
assetClass?: AssetClass;
|
||||||
|
assetSubClass?: AssetSubClass;
|
||||||
|
countries?: { code: string; weight: number }[];
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { AssetClass, Currency, DataSource } from '@prisma/client';
|
import {
|
||||||
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
|
Currency,
|
||||||
|
DataSource
|
||||||
|
} from '@prisma/client';
|
||||||
|
|
||||||
export interface EnhancedSymbolProfile {
|
export interface EnhancedSymbolProfile {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
|
assetSubClass: AssetSubClass;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
currency: Currency | null;
|
currency: Currency | null;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
@ -16,6 +16,7 @@ import { LinearScale } from 'chart.js';
|
|||||||
import { ArcElement } from 'chart.js';
|
import { ArcElement } from 'chart.js';
|
||||||
import { DoughnutController } from 'chart.js';
|
import { DoughnutController } from 'chart.js';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
|
import * as Color from 'color';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-portfolio-proportion-chart',
|
selector: 'gf-portfolio-proportion-chart',
|
||||||
@ -28,7 +29,7 @@ export class PortfolioProportionChartComponent
|
|||||||
{
|
{
|
||||||
@Input() baseCurrency: Currency;
|
@Input() baseCurrency: Currency;
|
||||||
@Input() isInPercent: boolean;
|
@Input() isInPercent: boolean;
|
||||||
@Input() key: string;
|
@Input() keys: string[];
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() maxItems?: number;
|
@Input() maxItems?: number;
|
||||||
@Input() positions: {
|
@Input() positions: {
|
||||||
@ -65,24 +66,54 @@ export class PortfolioProportionChartComponent
|
|||||||
private initialize() {
|
private initialize() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const chartData: {
|
const chartData: {
|
||||||
[symbol: string]: { color?: string; value: number };
|
[symbol: string]: {
|
||||||
|
color?: string;
|
||||||
|
subCategory: { [symbol: string]: { value: number } };
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
Object.keys(this.positions).forEach((symbol) => {
|
Object.keys(this.positions).forEach((symbol) => {
|
||||||
if (this.positions[symbol][this.key]) {
|
if (this.positions[symbol][this.keys[0]]) {
|
||||||
if (chartData[this.positions[symbol][this.key]]) {
|
if (chartData[this.positions[symbol][this.keys[0]]]) {
|
||||||
chartData[this.positions[symbol][this.key]].value +=
|
chartData[this.positions[symbol][this.keys[0]]].value +=
|
||||||
this.positions[symbol].value;
|
this.positions[symbol].value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
chartData[this.positions[symbol][this.keys[0]]].subCategory[
|
||||||
|
this.positions[symbol][this.keys[1]]
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
chartData[this.positions[symbol][this.keys[0]]].subCategory[
|
||||||
|
this.positions[symbol][this.keys[1]]
|
||||||
|
].value += this.positions[symbol].value;
|
||||||
|
} else {
|
||||||
|
chartData[this.positions[symbol][this.keys[0]]].subCategory[
|
||||||
|
this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY
|
||||||
|
] = { value: this.positions[symbol].value };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
chartData[this.positions[symbol][this.key]] = {
|
chartData[this.positions[symbol][this.keys[0]]] = {
|
||||||
|
subCategory: {},
|
||||||
value: this.positions[symbol].value
|
value: this.positions[symbol].value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.positions[symbol][this.keys[1]]) {
|
||||||
|
chartData[this.positions[symbol][this.keys[0]]].subCategory = {
|
||||||
|
[this.positions[symbol][this.keys[1]]]: {
|
||||||
|
value: this.positions[symbol].value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (chartData[UNKNOWN_KEY]) {
|
if (chartData[UNKNOWN_KEY]) {
|
||||||
chartData[UNKNOWN_KEY].value += this.positions[symbol].value;
|
chartData[UNKNOWN_KEY].value += this.positions[symbol].value;
|
||||||
} else {
|
} else {
|
||||||
chartData[UNKNOWN_KEY] = {
|
chartData[UNKNOWN_KEY] = {
|
||||||
|
subCategory: this.keys[1]
|
||||||
|
? { [this.keys[1]]: { value: 0 } }
|
||||||
|
: undefined,
|
||||||
value: this.positions[symbol].value
|
value: this.positions[symbol].value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -107,13 +138,17 @@ export class PortfolioProportionChartComponent
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!unknownItem) {
|
if (!unknownItem) {
|
||||||
const index = chartDataSorted.push([UNKNOWN_KEY, { value: 0 }]);
|
const index = chartDataSorted.push([
|
||||||
|
UNKNOWN_KEY,
|
||||||
|
{ subCategory: {}, value: 0 }
|
||||||
|
]);
|
||||||
unknownItem = chartDataSorted[index];
|
unknownItem = chartDataSorted[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
rest.forEach((restItem) => {
|
rest.forEach((restItem) => {
|
||||||
if (unknownItem?.[1]) {
|
if (unknownItem?.[1]) {
|
||||||
unknownItem[1] = {
|
unknownItem[1] = {
|
||||||
|
subCategory: {},
|
||||||
value: unknownItem[1].value + restItem[1].value
|
value: unknownItem[1].value + restItem[1].value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -141,21 +176,53 @@ export class PortfolioProportionChartComponent
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const backgroundColorSubCategory: string[] = [];
|
||||||
|
const dataSubCategory: number[] = [];
|
||||||
|
const labelSubCategory: string[] = [];
|
||||||
|
|
||||||
|
chartDataSorted.forEach(([, item]) => {
|
||||||
|
let lightnessRatio = 0.2;
|
||||||
|
|
||||||
|
Object.keys(item.subCategory).forEach((subCategory) => {
|
||||||
|
backgroundColorSubCategory.push(
|
||||||
|
Color(item.color).lighten(lightnessRatio).hex()
|
||||||
|
);
|
||||||
|
dataSubCategory.push(item.subCategory[subCategory].value);
|
||||||
|
labelSubCategory.push(subCategory);
|
||||||
|
|
||||||
|
lightnessRatio += 0.1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const datasets = [
|
||||||
|
{
|
||||||
|
backgroundColor: chartDataSorted.map(([, item]) => {
|
||||||
|
return item.color;
|
||||||
|
}),
|
||||||
|
borderWidth: 0,
|
||||||
|
data: chartDataSorted.map(([, item]) => {
|
||||||
|
return item.value;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let labels = chartDataSorted.map(([label]) => {
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.keys[1]) {
|
||||||
|
datasets.unshift({
|
||||||
|
backgroundColor: backgroundColorSubCategory,
|
||||||
|
borderWidth: 0,
|
||||||
|
data: dataSubCategory
|
||||||
|
});
|
||||||
|
|
||||||
|
labels = labelSubCategory.concat(labels);
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
datasets: [
|
datasets,
|
||||||
{
|
labels
|
||||||
backgroundColor: chartDataSorted.map(([, item]) => {
|
|
||||||
return item.color;
|
|
||||||
}),
|
|
||||||
borderWidth: 0,
|
|
||||||
data: chartDataSorted.map(([, item]) => {
|
|
||||||
return item.value;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
],
|
|
||||||
labels: chartDataSorted.map(([label]) => {
|
|
||||||
return label;
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.chartCanvas) {
|
if (this.chartCanvas) {
|
||||||
@ -166,13 +233,16 @@ export class PortfolioProportionChartComponent
|
|||||||
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
||||||
data,
|
data,
|
||||||
options: {
|
options: {
|
||||||
|
cutout: '70%',
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context) => {
|
label: (context) => {
|
||||||
const label =
|
const labelIndex =
|
||||||
context.label === UNKNOWN_KEY ? 'Other' : context.label;
|
(data.datasets[context.datasetIndex - 1]?.data?.length ??
|
||||||
|
0) + context.dataIndex;
|
||||||
|
const label = context.chart.data.labels[labelIndex];
|
||||||
|
|
||||||
if (this.isInPercent) {
|
if (this.isInPercent) {
|
||||||
const value = 100 * <number>context.raw;
|
const value = 100 * <number>context.raw;
|
||||||
|
@ -35,12 +35,7 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the controller
|
|
||||||
*/
|
|
||||||
public ngOnInit() {
|
|
||||||
const { globalPermissions, statistics } = this.dataService.fetchInfo();
|
const { globalPermissions, statistics } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
this.hasPermissionForBlog = hasPermission(
|
this.hasPermissionForBlog = hasPermission(
|
||||||
@ -59,7 +54,12 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.statistics = statistics;
|
this.statistics = statistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
|
@ -116,7 +116,20 @@
|
|||||||
<tr *ngFor="let userItem of users; let i = index" class="mat-row">
|
<tr *ngFor="let userItem of users; let i = index" class="mat-row">
|
||||||
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
||||||
<td class="mat-cell px-1 py-2">
|
<td class="mat-cell px-1 py-2">
|
||||||
{{ userItem.alias || userItem.id }}
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="d-none d-sm-inline-block"
|
||||||
|
>{{ userItem.alias || userItem.id }}</span
|
||||||
|
>
|
||||||
|
<span class="d-inline-block d-sm-none"
|
||||||
|
>{{ userItem.alias || (userItem.id | slice:0:5) +
|
||||||
|
'...' }}</span
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
*ngIf="userItem?.subscription?.type === 'Premium'"
|
||||||
|
class="ml-1 text-muted"
|
||||||
|
name="diamond-outline"
|
||||||
|
></ion-icon>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2 text-right">
|
<td class="mat-cell px-1 py-2 text-right">
|
||||||
{{ formatDistanceToNow(userItem.createdAt) }}
|
{{ formatDistanceToNow(userItem.createdAt) }}
|
||||||
|
@ -3,7 +3,7 @@ import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/to
|
|||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { ghostfolioCashSymbol, UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
@ -129,6 +129,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
)) {
|
)) {
|
||||||
this.positions[symbol] = {
|
this.positions[symbol] = {
|
||||||
assetClass: position.assetClass,
|
assetClass: position.assetClass,
|
||||||
|
assetSubClass: position.assetSubClass,
|
||||||
currency: position.currency,
|
currency: position.currency,
|
||||||
exchange: position.exchange,
|
exchange: position.exchange,
|
||||||
value:
|
value:
|
||||||
|
@ -18,9 +18,9 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
key="name"
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="hasImpersonationId"
|
[isInPercent]="hasImpersonationId"
|
||||||
|
[keys]="['name']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="accounts"
|
[positions]="accounts"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
@ -40,9 +40,9 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
key="assetClass"
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="true"
|
[isInPercent]="true"
|
||||||
|
[keys]="['assetClass', 'assetSubClass']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="positions"
|
[positions]="positions"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
@ -62,9 +62,9 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
key="currency"
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="true"
|
[isInPercent]="true"
|
||||||
|
[keys]="['currency']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="positions"
|
[positions]="positions"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
@ -84,9 +84,9 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
key="name"
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="false"
|
[isInPercent]="false"
|
||||||
|
[keys]="['name']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[maxItems]="10"
|
[maxItems]="10"
|
||||||
[positions]="sectors"
|
[positions]="sectors"
|
||||||
@ -107,9 +107,9 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
key="name"
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="false"
|
[isInPercent]="false"
|
||||||
|
[keys]="['name']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="continents"
|
[positions]="continents"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
@ -129,7 +129,7 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
key="name"
|
[keys]="['name']"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="false"
|
[isInPercent]="false"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { AssetClass, Currency } from '@prisma/client';
|
import { AssetClass, AssetSubClass, Currency } from '@prisma/client';
|
||||||
|
|
||||||
import { Country } from './country.interface';
|
import { Country } from './country.interface';
|
||||||
import { Sector } from './sector.interface';
|
import { Sector } from './sector.interface';
|
||||||
@ -8,6 +8,7 @@ export interface PortfolioPosition {
|
|||||||
allocationCurrent: number;
|
allocationCurrent: number;
|
||||||
allocationInvestment: number;
|
allocationInvestment: number;
|
||||||
assetClass?: AssetClass;
|
assetClass?: AssetClass;
|
||||||
|
assetSubClass?: AssetSubClass;
|
||||||
countries: Country[];
|
countries: Country[];
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.41.0",
|
"version": "1.43.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -82,6 +82,7 @@
|
|||||||
"cheerio": "1.0.0-rc.6",
|
"cheerio": "1.0.0-rc.6",
|
||||||
"class-transformer": "0.3.2",
|
"class-transformer": "0.3.2",
|
||||||
"class-validator": "0.13.1",
|
"class-validator": "0.13.1",
|
||||||
|
"color": "4.0.1",
|
||||||
"countries-list": "2.6.1",
|
"countries-list": "2.6.1",
|
||||||
"countup.js": "2.0.7",
|
"countup.js": "2.0.7",
|
||||||
"cryptocurrencies": "7.0.0",
|
"cryptocurrencies": "7.0.0",
|
||||||
@ -126,6 +127,7 @@
|
|||||||
"@nrwl/workspace": "12.5.4",
|
"@nrwl/workspace": "12.5.4",
|
||||||
"@types/big.js": "6.1.1",
|
"@types/big.js": "6.1.1",
|
||||||
"@types/cache-manager": "3.4.0",
|
"@types/cache-manager": "3.4.0",
|
||||||
|
"@types/color": "3.0.2",
|
||||||
"@types/jest": "26.0.20",
|
"@types/jest": "26.0.20",
|
||||||
"@types/lodash": "4.14.168",
|
"@types/lodash": "4.14.168",
|
||||||
"@types/node": "14.14.33",
|
"@types/node": "14.14.33",
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "AssetSubClass" AS ENUM ('CRYPTOCURRENCY', 'ETF', 'STOCK');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";
|
@ -117,17 +117,18 @@ model Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model SymbolProfile {
|
model SymbolProfile {
|
||||||
assetClass AssetClass?
|
assetClass AssetClass?
|
||||||
countries Json?
|
assetSubClass AssetSubClass?
|
||||||
createdAt DateTime @default(now())
|
countries Json?
|
||||||
currency Currency?
|
createdAt DateTime @default(now())
|
||||||
dataSource DataSource
|
currency Currency?
|
||||||
id String @id @default(uuid())
|
dataSource DataSource
|
||||||
name String?
|
id String @id @default(uuid())
|
||||||
Order Order[]
|
name String?
|
||||||
updatedAt DateTime @updatedAt
|
Order Order[]
|
||||||
sectors Json?
|
updatedAt DateTime @updatedAt
|
||||||
symbol String
|
sectors Json?
|
||||||
|
symbol String
|
||||||
|
|
||||||
@@unique([dataSource, symbol])
|
@@unique([dataSource, symbol])
|
||||||
}
|
}
|
||||||
@ -174,6 +175,12 @@ enum AssetClass {
|
|||||||
EQUITY
|
EQUITY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AssetSubClass {
|
||||||
|
CRYPTOCURRENCY
|
||||||
|
ETF
|
||||||
|
STOCK
|
||||||
|
}
|
||||||
|
|
||||||
enum Currency {
|
enum Currency {
|
||||||
CHF
|
CHF
|
||||||
EUR
|
EUR
|
||||||
|
49
yarn.lock
49
yarn.lock
@ -2567,6 +2567,25 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/cache-manager/-/cache-manager-3.4.0.tgz#414136ea3807a8cd071b8f20370c5df5dbffd382"
|
resolved "https://registry.yarnpkg.com/@types/cache-manager/-/cache-manager-3.4.0.tgz#414136ea3807a8cd071b8f20370c5df5dbffd382"
|
||||||
integrity sha512-XVbn2HS+O+Mk2SKRCjr01/8oD5p2Tv1fxxdBqJ0+Cl+UBNiz0WVY5rusHpMGx+qF6Vc2pnRwPVwSKbGaDApCpw==
|
integrity sha512-XVbn2HS+O+Mk2SKRCjr01/8oD5p2Tv1fxxdBqJ0+Cl+UBNiz0WVY5rusHpMGx+qF6Vc2pnRwPVwSKbGaDApCpw==
|
||||||
|
|
||||||
|
"@types/color-convert@*":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22"
|
||||||
|
integrity sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/color-name" "*"
|
||||||
|
|
||||||
|
"@types/color-name@*":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||||
|
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||||
|
|
||||||
|
"@types/color@3.0.2":
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.2.tgz#3779043e782f562aa9157b5fc6bd07e14fd8e7f3"
|
||||||
|
integrity sha512-INiJl6sfNn8iyC5paxVzqiVUEj2boIlFki02uRTAkKwAj++7aAF+ZfEv/XrIeBa0XI/fTZuDHW8rEEcEVnON+Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/color-convert" "*"
|
||||||
|
|
||||||
"@types/connect@*":
|
"@types/connect@*":
|
||||||
version "3.4.34"
|
version "3.4.34"
|
||||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901"
|
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901"
|
||||||
@ -4804,11 +4823,27 @@ color-name@1.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||||
|
|
||||||
color-name@~1.1.4:
|
color-name@^1.0.0, color-name@~1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
|
color-string@^1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
|
||||||
|
integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
|
||||||
|
dependencies:
|
||||||
|
color-name "^1.0.0"
|
||||||
|
simple-swizzle "^0.2.2"
|
||||||
|
|
||||||
|
color@4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67"
|
||||||
|
integrity sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==
|
||||||
|
dependencies:
|
||||||
|
color-convert "^2.0.1"
|
||||||
|
color-string "^1.6.0"
|
||||||
|
|
||||||
colord@^2.0.1:
|
colord@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.1.tgz#1e7fb1f9fa1cf74f42c58cb9c20320bab8435aa0"
|
resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.1.tgz#1e7fb1f9fa1cf74f42c58cb9c20320bab8435aa0"
|
||||||
@ -7780,6 +7815,11 @@ is-arrayish@^0.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||||
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
|
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
|
||||||
|
|
||||||
|
is-arrayish@^0.3.1:
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||||
|
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||||
|
|
||||||
is-bigint@^1.0.1:
|
is-bigint@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
|
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
|
||||||
@ -12349,6 +12389,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||||
|
|
||||||
|
simple-swizzle@^0.2.2:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||||
|
integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
|
||||||
|
dependencies:
|
||||||
|
is-arrayish "^0.3.1"
|
||||||
|
|
||||||
sisteransi@^1.0.5:
|
sisteransi@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
||||||
|
Reference in New Issue
Block a user