Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
06d5ec9182 | |||
122107c8a1 | |||
ca46a9827a | |||
4ec351369b | |||
dced06ebb5 | |||
baa6a3d0f0 | |||
d3382f0809 | |||
1eb4041837 |
13
CHANGELOG.md
13
CHANGELOG.md
@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.117.0 - 19.02.2022
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved the countries and sectors charts in the position detail dialog
|
||||
- Distinguished today's data point of historical data in the admin control panel
|
||||
- Restructured the server modules
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the allocations by account for non-unique account names
|
||||
- Added a fallback to the default account if the `accountId` is invalid in the import functionality for activities
|
||||
|
||||
## 1.116.0 - 16.02.2022
|
||||
|
||||
### Added
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AccessController } from './access.controller';
|
||||
@ -7,7 +7,7 @@ import { AccessService } from './access.service';
|
||||
@Module({
|
||||
controllers: [AccessController],
|
||||
exports: [AccessService],
|
||||
imports: [],
|
||||
providers: [AccessService, PrismaService]
|
||||
imports: [PrismaModule],
|
||||
providers: [AccessService]
|
||||
})
|
||||
export class AccessModule {}
|
||||
|
@ -13,6 +13,7 @@ import { AccountService } from './account.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AccountController],
|
||||
exports: [AccountService],
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
DataProviderModule,
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
|
||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthDeviceController],
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
signOptions: { expiresIn: '180 days' }
|
||||
})
|
||||
}),
|
||||
PrismaModule
|
||||
],
|
||||
providers: [AuthDeviceService, ConfigurationService, PrismaService]
|
||||
providers: [AuthDeviceService]
|
||||
})
|
||||
export class AuthDeviceModule {}
|
||||
|
@ -2,8 +2,8 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.s
|
||||
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
|
||||
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@ -15,20 +15,20 @@ import { JwtStrategy } from './jwt.strategy';
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
signOptions: { expiresIn: '180 days' }
|
||||
}),
|
||||
PrismaModule,
|
||||
SubscriptionModule,
|
||||
UserModule
|
||||
],
|
||||
providers: [
|
||||
AuthDeviceService,
|
||||
AuthService,
|
||||
ConfigurationService,
|
||||
GoogleStrategy,
|
||||
JwtStrategy,
|
||||
PrismaService,
|
||||
WebAuthService
|
||||
]
|
||||
})
|
||||
|
17
apps/api/src/app/cache/cache.module.ts
vendored
17
apps/api/src/app/cache/cache.module.ts
vendored
@ -1,30 +1,27 @@
|
||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CacheController } from './cache.controller';
|
||||
|
||||
@Module({
|
||||
exports: [CacheService],
|
||||
controllers: [CacheController],
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
ExchangeRateDataModule,
|
||||
PrismaModule,
|
||||
RedisCacheModule,
|
||||
SymbolProfileModule
|
||||
],
|
||||
controllers: [CacheController],
|
||||
providers: [
|
||||
CacheService,
|
||||
ConfigurationService,
|
||||
DataGatheringService,
|
||||
PrismaService
|
||||
]
|
||||
providers: [CacheService]
|
||||
})
|
||||
export class CacheModule {}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
|
||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
@ -11,7 +12,10 @@ import { ImportController } from './import.controller';
|
||||
import { ImportService } from './import.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ImportController],
|
||||
imports: [
|
||||
AccountModule,
|
||||
CacheModule,
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
@ -19,7 +23,6 @@ import { ImportService } from './import.service';
|
||||
PrismaModule,
|
||||
RedisCacheModule
|
||||
],
|
||||
controllers: [ImportController],
|
||||
providers: [CacheService, ImportService]
|
||||
providers: [ImportService]
|
||||
})
|
||||
export class ImportModule {}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
@ -8,6 +9,7 @@ import { isSameDay, parseISO } from 'date-fns';
|
||||
@Injectable()
|
||||
export class ImportService {
|
||||
public constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly orderService: OrderService
|
||||
@ -32,6 +34,12 @@ export class ImportService {
|
||||
|
||||
await this.validateOrders({ orders, userId });
|
||||
|
||||
const accountIds = (await this.accountService.getAccounts(userId)).map(
|
||||
(account) => {
|
||||
return account.id;
|
||||
}
|
||||
);
|
||||
|
||||
for (const {
|
||||
accountId,
|
||||
currency,
|
||||
@ -44,7 +52,6 @@ export class ImportService {
|
||||
unitPrice
|
||||
} of orders) {
|
||||
await this.orderService.createOrder({
|
||||
accountId,
|
||||
currency,
|
||||
dataSource,
|
||||
fee,
|
||||
@ -53,6 +60,7 @@ export class ImportService {
|
||||
type,
|
||||
unitPrice,
|
||||
userId,
|
||||
accountId: accountIds.includes(accountId) ? accountId : undefined,
|
||||
date: parseISO(<string>(<unknown>date)),
|
||||
SymbolProfile: {
|
||||
connectOrCreate: {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
@ -14,7 +13,9 @@ import { InfoController } from './info.controller';
|
||||
import { InfoService } from './info.service';
|
||||
|
||||
@Module({
|
||||
controllers: [InfoController],
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
ExchangeRateDataModule,
|
||||
@ -22,16 +23,11 @@ import { InfoService } from './info.service';
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
signOptions: { expiresIn: '30 days' }
|
||||
}),
|
||||
PrismaModule,
|
||||
PropertyModule,
|
||||
RedisCacheModule,
|
||||
SymbolProfileModule
|
||||
],
|
||||
controllers: [InfoController],
|
||||
providers: [
|
||||
ConfigurationService,
|
||||
DataGatheringService,
|
||||
InfoService,
|
||||
PrismaService
|
||||
]
|
||||
providers: [InfoService]
|
||||
})
|
||||
export class InfoModule {}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
@ -15,7 +15,10 @@ import { OrderController } from './order.controller';
|
||||
import { OrderService } from './order.service';
|
||||
|
||||
@Module({
|
||||
controllers: [OrderController],
|
||||
exports: [OrderService],
|
||||
imports: [
|
||||
CacheModule,
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
@ -26,8 +29,6 @@ import { OrderService } from './order.service';
|
||||
SymbolProfileModule,
|
||||
UserModule
|
||||
],
|
||||
controllers: [OrderController],
|
||||
providers: [AccountService, CacheService, OrderService],
|
||||
exports: [OrderService]
|
||||
providers: [AccountService, OrderService]
|
||||
})
|
||||
export class OrderModule {}
|
||||
|
@ -20,6 +20,7 @@ import { PortfolioServiceNew } from './portfolio.service-new';
|
||||
import { RulesService } from './rules.service';
|
||||
|
||||
@Module({
|
||||
controllers: [PortfolioController],
|
||||
exports: [PortfolioServiceStrategy],
|
||||
imports: [
|
||||
AccessModule,
|
||||
@ -34,7 +35,6 @@ import { RulesService } from './rules.service';
|
||||
SymbolProfileModule,
|
||||
UserModule
|
||||
],
|
||||
controllers: [PortfolioController],
|
||||
providers: [
|
||||
AccountService,
|
||||
CurrentRateService,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { CacheModule, Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
@ -17,9 +18,10 @@ import { RedisCacheService } from './redis-cache.service';
|
||||
store: redisStore,
|
||||
ttl: configurationService.get('CACHE_TTL')
|
||||
})
|
||||
})
|
||||
}),
|
||||
ConfigurationModule
|
||||
],
|
||||
providers: [ConfigurationService, RedisCacheService],
|
||||
providers: [RedisCacheService],
|
||||
exports: [RedisCacheService]
|
||||
})
|
||||
export class RedisCacheModule {}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@ -7,9 +7,9 @@ import { SubscriptionController } from './subscription.controller';
|
||||
import { SubscriptionService } from './subscription.service';
|
||||
|
||||
@Module({
|
||||
imports: [PropertyModule],
|
||||
controllers: [SubscriptionController],
|
||||
providers: [ConfigurationService, PrismaService, SubscriptionService],
|
||||
exports: [SubscriptionService]
|
||||
exports: [SubscriptionService],
|
||||
imports: [ConfigurationModule, PrismaModule, PropertyModule],
|
||||
providers: [SubscriptionService]
|
||||
})
|
||||
export class SubscriptionModule {}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
@ -9,16 +9,18 @@ import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Module({
|
||||
controllers: [UserController],
|
||||
exports: [UserService],
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
signOptions: { expiresIn: '30 days' }
|
||||
}),
|
||||
PrismaModule,
|
||||
PropertyModule,
|
||||
SubscriptionModule
|
||||
],
|
||||
controllers: [UserController],
|
||||
providers: [ConfigurationService, PrismaService, UserService],
|
||||
exports: [UserService]
|
||||
providers: [UserService]
|
||||
})
|
||||
export class UserModule {}
|
||||
|
@ -23,8 +23,8 @@ export class CronService {
|
||||
await this.exchangeRateDataService.loadCurrencies();
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_DAY_AT_6PM)
|
||||
public async runEveryDayAtSixPM() {
|
||||
@Cron(CronExpression.EVERY_DAY_AT_5PM)
|
||||
public async runEveryDayAtFivePM() {
|
||||
this.twitterBotService.tweetFearAndGreedIndex();
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
} from '@ghostfolio/common/config';
|
||||
import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { isSunday } from 'date-fns';
|
||||
import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
|
||||
|
||||
@Injectable()
|
||||
@ -27,7 +28,10 @@ export class TwitterBotService {
|
||||
}
|
||||
|
||||
public async tweetFearAndGreedIndex() {
|
||||
if (!this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
||||
if (
|
||||
!this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX') ||
|
||||
isSunday(new Date())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,10 @@
|
||||
marketDataByMonth[itemByMonth.key][
|
||||
i + 1 < 10 ? '0' + (i + 1) : i + 1
|
||||
]?.day ===
|
||||
i + 1
|
||||
i + 1,
|
||||
today: isToday(
|
||||
itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
)
|
||||
}"
|
||||
[title]="
|
||||
(itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
|
@ -25,5 +25,10 @@
|
||||
&.available {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
&.today {
|
||||
background-color: rgba(var(--palette-accent-500), 1);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
import { format, isBefore, isValid, parse } from 'date-fns';
|
||||
import { format, isBefore, isSameDay, isValid, parse } from 'date-fns';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@ -82,6 +82,11 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
|
||||
return isValid(date) && isBefore(date, new Date());
|
||||
}
|
||||
|
||||
public isToday(aDateString: string) {
|
||||
const date = parse(aDateString, DATE_FORMAT, new Date());
|
||||
return isValid(date) && isSameDay(date, new Date());
|
||||
}
|
||||
|
||||
public onOpenMarketDataDetail({
|
||||
day,
|
||||
yearMonth
|
||||
@ -89,13 +94,18 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
|
||||
day: string;
|
||||
yearMonth: string;
|
||||
}) {
|
||||
const date = new Date(`${yearMonth}-${day}`);
|
||||
const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice;
|
||||
|
||||
if (isSameDay(date, new Date())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = this.dialog.open(MarketDataDetailDialog, {
|
||||
data: {
|
||||
date,
|
||||
marketPrice,
|
||||
dataSource: this.dataSource,
|
||||
date: new Date(`${yearMonth}-${day}`),
|
||||
symbol: this.symbol
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
|
@ -144,6 +144,14 @@
|
||||
<ng-container
|
||||
*ngIf="SymbolProfile?.countries?.length === 1 && SymbolProfile?.sectors?.length === 1; else charts"
|
||||
>
|
||||
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Sector"
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="SymbolProfile.sectors[0].name"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="SymbolProfile?.countries?.length === 1"
|
||||
class="col-6 mb-3"
|
||||
@ -155,27 +163,8 @@
|
||||
[value]="SymbolProfile.countries[0].name"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Sector"
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="SymbolProfile.sectors[0].name"
|
||||
></gf-value>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #charts>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h4" i18n>Countries</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h4" i18n>Sectors</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -187,6 +176,17 @@
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h4" i18n>Countries</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { ToggleOption } from '@ghostfolio/common/types';
|
||||
import { AssetClass, DataSource } from '@prisma/client';
|
||||
import { Account, AssetClass, DataSource } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -27,7 +27,10 @@ import { takeUntil } from 'rxjs/operators';
|
||||
})
|
||||
export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
public accounts: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
||||
[id: string]: Pick<Account, 'name'> & {
|
||||
id: string;
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
public continents: {
|
||||
[code: string]: { name: string; value: number };
|
||||
@ -171,6 +174,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.portfolioDetails.accounts
|
||||
)) {
|
||||
this.accounts[id] = {
|
||||
id,
|
||||
name,
|
||||
value: aPeriod === 'original' ? original : current
|
||||
};
|
||||
|
@ -20,7 +20,7 @@
|
||||
<gf-portfolio-proportion-chart
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[keys]="['name']"
|
||||
[keys]="['id']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="accounts"
|
||||
></gf-portfolio-proportion-chart>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { getTextColor } from '@ghostfolio/common/helper';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import Big from 'big.js';
|
||||
import { Tooltip } from 'chart.js';
|
||||
import { LinearScale } from 'chart.js';
|
||||
import { ArcElement } from 'chart.js';
|
||||
@ -78,16 +79,17 @@ export class PortfolioProportionChartComponent
|
||||
[symbol: string]: {
|
||||
color?: string;
|
||||
name: string;
|
||||
subCategory: { [symbol: string]: { value: number } };
|
||||
value: number;
|
||||
subCategory: { [symbol: string]: { value: Big } };
|
||||
value: Big;
|
||||
};
|
||||
} = {};
|
||||
|
||||
Object.keys(this.positions).forEach((symbol) => {
|
||||
if (this.positions[symbol][this.keys[0]]) {
|
||||
if (chartData[this.positions[symbol][this.keys[0]]]) {
|
||||
chartData[this.positions[symbol][this.keys[0]]].value +=
|
||||
this.positions[symbol].value;
|
||||
chartData[this.positions[symbol][this.keys[0]]].value = chartData[
|
||||
this.positions[symbol][this.keys[0]]
|
||||
].value.plus(this.positions[symbol].value);
|
||||
|
||||
if (
|
||||
chartData[this.positions[symbol][this.keys[0]]].subCategory[
|
||||
@ -96,37 +98,43 @@ export class PortfolioProportionChartComponent
|
||||
) {
|
||||
chartData[this.positions[symbol][this.keys[0]]].subCategory[
|
||||
this.positions[symbol][this.keys[1]]
|
||||
].value += this.positions[symbol].value;
|
||||
].value = chartData[
|
||||
this.positions[symbol][this.keys[0]]
|
||||
].subCategory[this.positions[symbol][this.keys[1]]].value.plus(
|
||||
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 };
|
||||
] = { value: new Big(this.positions[symbol].value) };
|
||||
}
|
||||
} else {
|
||||
chartData[this.positions[symbol][this.keys[0]]] = {
|
||||
name: this.positions[symbol].name,
|
||||
subCategory: {},
|
||||
value: this.positions[symbol].value
|
||||
value: new Big(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
|
||||
value: new Big(this.positions[symbol].value)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (chartData[UNKNOWN_KEY]) {
|
||||
chartData[UNKNOWN_KEY].value += this.positions[symbol].value;
|
||||
chartData[UNKNOWN_KEY].value = chartData[UNKNOWN_KEY].value.plus(
|
||||
this.positions[symbol].value
|
||||
);
|
||||
} else {
|
||||
chartData[UNKNOWN_KEY] = {
|
||||
name: this.positions[symbol].name,
|
||||
subCategory: this.keys[1]
|
||||
? { [this.keys[1]]: { value: 0 } }
|
||||
? { [this.keys[1]]: { value: new Big(0) } }
|
||||
: undefined,
|
||||
value: this.positions[symbol].value
|
||||
value: new Big(this.positions[symbol].value)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -134,7 +142,7 @@ export class PortfolioProportionChartComponent
|
||||
|
||||
let chartDataSorted = Object.entries(chartData)
|
||||
.sort((a, b) => {
|
||||
return a[1].value - b[1].value;
|
||||
return a[1].value.minus(b[1].value).toNumber();
|
||||
})
|
||||
.reverse();
|
||||
|
||||
@ -152,7 +160,7 @@ export class PortfolioProportionChartComponent
|
||||
if (!unknownItem) {
|
||||
chartDataSorted.push([
|
||||
UNKNOWN_KEY,
|
||||
{ name: UNKNOWN_KEY, subCategory: {}, value: 0 }
|
||||
{ name: UNKNOWN_KEY, subCategory: {}, value: new Big(0) }
|
||||
]);
|
||||
unknownItem = chartDataSorted[chartDataSorted.length - 1];
|
||||
}
|
||||
@ -162,7 +170,7 @@ export class PortfolioProportionChartComponent
|
||||
unknownItem[1] = {
|
||||
name: UNKNOWN_KEY,
|
||||
subCategory: {},
|
||||
value: unknownItem[1].value + restItem[1].value
|
||||
value: unknownItem[1].value.plus(restItem[1].value)
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -170,7 +178,7 @@ export class PortfolioProportionChartComponent
|
||||
// Sort data again
|
||||
chartDataSorted = chartDataSorted
|
||||
.sort((a, b) => {
|
||||
return a[1].value - b[1].value;
|
||||
return a[1].value.minus(b[1].value).toNumber();
|
||||
})
|
||||
.reverse();
|
||||
}
|
||||
@ -201,7 +209,7 @@ export class PortfolioProportionChartComponent
|
||||
backgroundColorSubCategory.push(
|
||||
Color(item.color).lighten(lightnessRatio).hex()
|
||||
);
|
||||
dataSubCategory.push(item.subCategory[subCategory].value);
|
||||
dataSubCategory.push(item.subCategory[subCategory].value.toNumber());
|
||||
labelSubCategory.push(subCategory);
|
||||
|
||||
lightnessRatio += 0.1;
|
||||
@ -215,7 +223,7 @@ export class PortfolioProportionChartComponent
|
||||
}),
|
||||
borderWidth: 0,
|
||||
data: chartDataSorted.map(([, item]) => {
|
||||
return item.value;
|
||||
return item.value.toNumber();
|
||||
})
|
||||
}
|
||||
];
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.116.0",
|
||||
"version": "1.117.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
Reference in New Issue
Block a user