Feature/add symbol profile model (#148)
* Add symbol profile model and positions by country chart * Add positions by continent chart * Fix tests * Extend seed * Update changelog
This commit is contained in:
parent
21504573b4
commit
6a03120225
@ -5,6 +5,13 @@ 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).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a symbol profile model with additional data
|
||||||
|
- Added new pie charts: Positions by continent and country
|
||||||
|
|
||||||
## 1.11.0 - 05.06.2021
|
## 1.11.0 - 05.06.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -44,6 +44,7 @@ export class ExperimentalService {
|
|||||||
fee: 0,
|
fee: 0,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
platformId: undefined,
|
platformId: undefined,
|
||||||
|
symbolProfileId: undefined,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
updatedAt: undefined,
|
updatedAt: undefined,
|
||||||
userId: undefined
|
userId: undefined
|
||||||
|
@ -76,7 +76,8 @@ export class PortfolioService {
|
|||||||
// Get portfolio from database
|
// Get portfolio from database
|
||||||
const orders = await this.orderService.orders({
|
const orders = await this.orderService.orders({
|
||||||
include: {
|
include: {
|
||||||
Account: true
|
Account: true,
|
||||||
|
SymbolProfile: true
|
||||||
},
|
},
|
||||||
orderBy: { date: 'asc' },
|
orderBy: { date: 'asc' },
|
||||||
where: { userId: aUserId }
|
where: { userId: aUserId }
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Account, Currency, Platform } from '@prisma/client';
|
import { Account, Currency, Platform, SymbolProfile } from '@prisma/client';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { IOrder } from '../services/interfaces/interfaces';
|
import { IOrder } from '../services/interfaces/interfaces';
|
||||||
@ -12,6 +12,7 @@ export class Order {
|
|||||||
private id: string;
|
private id: string;
|
||||||
private quantity: number;
|
private quantity: number;
|
||||||
private symbol: string;
|
private symbol: string;
|
||||||
|
private symbolProfile: SymbolProfile;
|
||||||
private total: number;
|
private total: number;
|
||||||
private type: OrderType;
|
private type: OrderType;
|
||||||
private unitPrice: number;
|
private unitPrice: number;
|
||||||
@ -24,6 +25,7 @@ export class Order {
|
|||||||
this.id = data.id || uuidv4();
|
this.id = data.id || uuidv4();
|
||||||
this.quantity = data.quantity;
|
this.quantity = data.quantity;
|
||||||
this.symbol = data.symbol;
|
this.symbol = data.symbol;
|
||||||
|
this.symbolProfile = data.symbolProfile;
|
||||||
this.type = data.type;
|
this.type = data.type;
|
||||||
this.unitPrice = data.unitPrice;
|
this.unitPrice = data.unitPrice;
|
||||||
|
|
||||||
@ -58,6 +60,10 @@ export class Order {
|
|||||||
return this.symbol;
|
return this.symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSymbolProfile() {
|
||||||
|
return this.symbolProfile;
|
||||||
|
}
|
||||||
|
|
||||||
public getTotal() {
|
public getTotal() {
|
||||||
return this.total;
|
return this.total;
|
||||||
}
|
}
|
||||||
|
@ -189,6 +189,7 @@ describe('Portfolio', () => {
|
|||||||
id: '8d999347-dee2-46ee-88e1-26b344e71fcc',
|
id: '8d999347-dee2-46ee-88e1-26b344e71fcc',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 49631.24,
|
unitPrice: 49631.24,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
@ -223,6 +224,7 @@ describe('Portfolio', () => {
|
|||||||
},
|
},
|
||||||
allocationCurrent: 1,
|
allocationCurrent: 1,
|
||||||
allocationInvestment: 1,
|
allocationInvestment: 1,
|
||||||
|
countries: [],
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
exchange: UNKNOWN_KEY,
|
exchange: UNKNOWN_KEY,
|
||||||
grossPerformance: 0,
|
grossPerformance: 0,
|
||||||
@ -290,6 +292,7 @@ describe('Portfolio', () => {
|
|||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 991.49,
|
unitPrice: 991.49,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
@ -324,6 +327,7 @@ describe('Portfolio', () => {
|
|||||||
},
|
},
|
||||||
// allocationCurrent: 1,
|
// allocationCurrent: 1,
|
||||||
allocationInvestment: 1,
|
allocationInvestment: 1,
|
||||||
|
countries: [],
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
exchange: UNKNOWN_KEY,
|
exchange: UNKNOWN_KEY,
|
||||||
// grossPerformance: 0,
|
// grossPerformance: 0,
|
||||||
@ -385,6 +389,7 @@ describe('Portfolio', () => {
|
|||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 991.49,
|
unitPrice: 991.49,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
@ -401,6 +406,7 @@ describe('Portfolio', () => {
|
|||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||||
quantity: 0.3,
|
quantity: 0.3,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 1050,
|
unitPrice: 1050,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
@ -461,6 +467,7 @@ describe('Portfolio', () => {
|
|||||||
id: 'd96795b2-6ae6-420e-aa21-fabe5e45d475',
|
id: 'd96795b2-6ae6-420e-aa21-fabe5e45d475',
|
||||||
quantity: 0.05614682,
|
quantity: 0.05614682,
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 3562.089535970158,
|
unitPrice: 3562.089535970158,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
@ -477,6 +484,7 @@ describe('Portfolio', () => {
|
|||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 991.49,
|
unitPrice: 991.49,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
@ -550,6 +558,7 @@ describe('Portfolio', () => {
|
|||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 991.49,
|
unitPrice: 991.49,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
@ -566,6 +575,7 @@ describe('Portfolio', () => {
|
|||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||||
quantity: 0.1,
|
quantity: 0.1,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.SELL,
|
type: Type.SELL,
|
||||||
unitPrice: 1050,
|
unitPrice: 1050,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
@ -582,6 +592,7 @@ describe('Portfolio', () => {
|
|||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
|
symbolProfileId: null,
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 1050,
|
unitPrice: 1050,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
|
@ -8,7 +8,10 @@ import {
|
|||||||
Position,
|
Position,
|
||||||
UserWithSettings
|
UserWithSettings
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { DateRange, OrderWithAccount } from '@ghostfolio/common/types';
|
import { DateRange, OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { continents, countries } from 'countries-list';
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
format,
|
format,
|
||||||
@ -127,6 +130,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
id,
|
id,
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
|
symbolProfile,
|
||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
}) => {
|
}) => {
|
||||||
@ -139,6 +143,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
id,
|
id,
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
|
symbolProfile,
|
||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
})
|
})
|
||||||
@ -204,6 +209,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
|
|
||||||
symbols.forEach((symbol) => {
|
symbols.forEach((symbol) => {
|
||||||
const accounts: PortfolioPosition['accounts'] = {};
|
const accounts: PortfolioPosition['accounts'] = {};
|
||||||
|
let countriesOfSymbol: Country[];
|
||||||
const [portfolioItem] = portfolioItems;
|
const [portfolioItem] = portfolioItems;
|
||||||
|
|
||||||
const ordersBySymbol = this.getOrders().filter((order) => {
|
const ordersBySymbol = this.getOrders().filter((order) => {
|
||||||
@ -243,6 +249,21 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
original: originalValueOfSymbol
|
original: originalValueOfSymbol
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countriesOfSymbol = (
|
||||||
|
(orderOfSymbol.getSymbolProfile()?.countries as Prisma.JsonArray) ??
|
||||||
|
[]
|
||||||
|
).map((country) => {
|
||||||
|
const { code, weight } = country as Prisma.JsonObject;
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: code as string,
|
||||||
|
continent:
|
||||||
|
continents[countries[code as string]?.continent] ?? UNKNOWN_KEY,
|
||||||
|
name: countries[code as string]?.name ?? UNKNOWN_KEY,
|
||||||
|
weight: weight as number
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let now = portfolioItemsNow.positions[symbol].marketPrice;
|
let now = portfolioItemsNow.positions[symbol].marketPrice;
|
||||||
@ -289,6 +310,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
) / value,
|
) / value,
|
||||||
allocationInvestment:
|
allocationInvestment:
|
||||||
portfolioItem.positions[symbol].investment / investment,
|
portfolioItem.positions[symbol].investment / investment,
|
||||||
|
countries: countriesOfSymbol,
|
||||||
grossPerformance: roundTo(
|
grossPerformance: roundTo(
|
||||||
portfolioItemsNow.positions[symbol].quantity * (now - before),
|
portfolioItemsNow.positions[symbol].quantity * (now - before),
|
||||||
2
|
2
|
||||||
@ -296,7 +318,12 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
grossPerformancePercent: roundTo((now - before) / before, 4),
|
grossPerformancePercent: roundTo((now - before) / before, 4),
|
||||||
investment: portfolioItem.positions[symbol].investment,
|
investment: portfolioItem.positions[symbol].investment,
|
||||||
quantity: portfolioItem.positions[symbol].quantity,
|
quantity: portfolioItem.positions[symbol].quantity,
|
||||||
transactionCount: portfolioItem.positions[symbol].transactionCount
|
transactionCount: portfolioItem.positions[symbol].transactionCount,
|
||||||
|
value: this.exchangeRateDataService.toCurrency(
|
||||||
|
portfolioItem.positions[symbol].quantity * now,
|
||||||
|
data[symbol]?.currency,
|
||||||
|
this.user.Settings.currency
|
||||||
|
)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -544,6 +571,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
fee: order.fee,
|
fee: order.fee,
|
||||||
quantity: order.quantity,
|
quantity: order.quantity,
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
|
symbolProfile: order.SymbolProfile,
|
||||||
type: <OrderType>order.type,
|
type: <OrderType>order.type,
|
||||||
unitPrice: order.unitPrice
|
unitPrice: order.unitPrice
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import { Account, Currency, DataSource } from '@prisma/client';
|
import { Account, Currency, DataSource, SymbolProfile } from '@prisma/client';
|
||||||
|
|
||||||
import { OrderType } from '../../models/order-type';
|
import { OrderType } from '../../models/order-type';
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ export interface IOrder {
|
|||||||
id?: string;
|
id?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
symbolProfile: SymbolProfile;
|
||||||
type: OrderType;
|
type: OrderType;
|
||||||
unitPrice: number;
|
unitPrice: number;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +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 { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
@ -21,6 +22,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
public accounts: {
|
public accounts: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
||||||
};
|
};
|
||||||
|
public continents: {
|
||||||
|
[code: string]: { name: string; value: number };
|
||||||
|
};
|
||||||
|
public countries: {
|
||||||
|
[code: string]: { name: string; value: number };
|
||||||
|
};
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public period = 'current';
|
public period = 'current';
|
||||||
public periodOptions: ToggleOption[] = [
|
public periodOptions: ToggleOption[] = [
|
||||||
@ -97,6 +104,18 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
aPeriod: string
|
aPeriod: string
|
||||||
) {
|
) {
|
||||||
this.accounts = {};
|
this.accounts = {};
|
||||||
|
this.continents = {
|
||||||
|
[UNKNOWN_KEY]: {
|
||||||
|
name: UNKNOWN_KEY,
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.countries = {
|
||||||
|
[UNKNOWN_KEY]: {
|
||||||
|
name: UNKNOWN_KEY,
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
this.positions = {};
|
this.positions = {};
|
||||||
this.positionsArray = [];
|
this.positionsArray = [];
|
||||||
|
|
||||||
@ -122,11 +141,53 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
aPeriod === 'original' ? original : current;
|
aPeriod === 'original' ? original : current;
|
||||||
} else {
|
} else {
|
||||||
this.accounts[account] = {
|
this.accounts[account] = {
|
||||||
value: aPeriod === 'original' ? original : current,
|
name: account,
|
||||||
name: account
|
value: aPeriod === 'original' ? original : current
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (position.countries.length > 0) {
|
||||||
|
for (const country of position.countries) {
|
||||||
|
const { code, continent, name, weight } = country;
|
||||||
|
|
||||||
|
if (this.continents[continent]?.value) {
|
||||||
|
this.continents[continent].value += weight * position.value;
|
||||||
|
} else {
|
||||||
|
this.continents[continent] = {
|
||||||
|
name: continent,
|
||||||
|
value:
|
||||||
|
weight *
|
||||||
|
(aPeriod === 'original'
|
||||||
|
? this.portfolioPositions[symbol].investment
|
||||||
|
: this.portfolioPositions[symbol].value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.countries[code]?.value) {
|
||||||
|
this.countries[code].value += weight * position.value;
|
||||||
|
} else {
|
||||||
|
this.countries[code] = {
|
||||||
|
name,
|
||||||
|
value:
|
||||||
|
weight *
|
||||||
|
(aPeriod === 'original'
|
||||||
|
? this.portfolioPositions[symbol].investment
|
||||||
|
: this.portfolioPositions[symbol].value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.continents[UNKNOWN_KEY].value +=
|
||||||
|
aPeriod === 'original'
|
||||||
|
? this.portfolioPositions[symbol].investment
|
||||||
|
: this.portfolioPositions[symbol].value;
|
||||||
|
|
||||||
|
this.countries[UNKNOWN_KEY].value +=
|
||||||
|
aPeriod === 'original'
|
||||||
|
? this.portfolioPositions[symbol].investment
|
||||||
|
: this.portfolioPositions[symbol].value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +102,50 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header class="w-100">
|
||||||
|
<mat-card-title i18n>By Continent</mat-card-title>
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="period"
|
||||||
|
[isLoading]="false"
|
||||||
|
[options]="periodOptions"
|
||||||
|
(change)="onChangePeriod($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-portfolio-proportion-chart
|
||||||
|
key="name"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[isInPercent]="false"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[positions]="continents"
|
||||||
|
></gf-portfolio-proportion-chart>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header class="w-100">
|
||||||
|
<mat-card-title i18n>By Country</mat-card-title>
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="period"
|
||||||
|
[isLoading]="false"
|
||||||
|
[options]="periodOptions"
|
||||||
|
(change)="onChangePeriod($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-portfolio-proportion-chart
|
||||||
|
key="name"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[isInPercent]="false"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[positions]="countries"
|
||||||
|
></gf-portfolio-proportion-chart>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="w-100">
|
||||||
|
6
libs/common/src/lib/interfaces/country.interface.ts
Normal file
6
libs/common/src/lib/interfaces/country.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface Country {
|
||||||
|
code: string;
|
||||||
|
continent: string;
|
||||||
|
name: string;
|
||||||
|
weight: number;
|
||||||
|
}
|
@ -1,12 +1,15 @@
|
|||||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
|
import { Country } from './country.interface';
|
||||||
|
|
||||||
export interface PortfolioPosition {
|
export interface PortfolioPosition {
|
||||||
accounts: {
|
accounts: {
|
||||||
[name: string]: { current: number; original: number };
|
[name: string]: { current: number; original: number };
|
||||||
};
|
};
|
||||||
allocationCurrent: number;
|
allocationCurrent: number;
|
||||||
allocationInvestment: number;
|
allocationInvestment: number;
|
||||||
|
countries: Country[];
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
grossPerformance: number;
|
grossPerformance: number;
|
||||||
@ -24,4 +27,5 @@ export interface PortfolioPosition {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
value: number;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { Account, Order, Platform } from '@prisma/client';
|
import { Account, Order, Platform, SymbolProfile } from '@prisma/client';
|
||||||
|
|
||||||
type AccountWithPlatform = Account & { Platform?: Platform };
|
type AccountWithPlatform = Account & { Platform?: Platform };
|
||||||
|
|
||||||
export type OrderWithAccount = Order & { Account?: AccountWithPlatform };
|
export type OrderWithAccount = Order & {
|
||||||
|
Account?: AccountWithPlatform;
|
||||||
|
SymbolProfile?: SymbolProfile;
|
||||||
|
};
|
||||||
|
@ -79,6 +79,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",
|
||||||
|
"countries-list": "2.6.1",
|
||||||
"countup.js": "2.0.7",
|
"countup.js": "2.0.7",
|
||||||
"cryptocurrencies": "7.0.0",
|
"cryptocurrencies": "7.0.0",
|
||||||
"date-fns": "2.19.0",
|
"date-fns": "2.19.0",
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SymbolProfile" (
|
||||||
|
"countries" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"dataSource" "DataSource" NOT NULL,
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"symbol" TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "SymbolProfile.dataSource_symbol_unique" ON "SymbolProfile"("dataSource", "symbol");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Order" ADD FOREIGN KEY ("symbolProfileId") REFERENCES "SymbolProfile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -59,22 +59,24 @@ model MarketData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Order {
|
model Order {
|
||||||
Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId])
|
Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId])
|
||||||
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
|
||||||
id String @default(uuid())
|
id String @default(uuid())
|
||||||
quantity Float
|
quantity Float
|
||||||
symbol String
|
symbol String
|
||||||
type Type
|
SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id])
|
||||||
unitPrice Float
|
symbolProfileId String?
|
||||||
updatedAt DateTime @updatedAt
|
type Type
|
||||||
User User @relation(fields: [userId], references: [id])
|
unitPrice Float
|
||||||
userId String
|
updatedAt DateTime @updatedAt
|
||||||
|
User User @relation(fields: [userId], references: [id])
|
||||||
|
userId String
|
||||||
|
|
||||||
@@id([id, userId])
|
@@id([id, userId])
|
||||||
}
|
}
|
||||||
@ -99,6 +101,19 @@ model Settings {
|
|||||||
userId String @id
|
userId String @id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SymbolProfile {
|
||||||
|
countries Json?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
dataSource DataSource
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String?
|
||||||
|
Order Order[]
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
symbol String
|
||||||
|
|
||||||
|
@@unique([dataSource, symbol])
|
||||||
|
}
|
||||||
|
|
||||||
model Subscription {
|
model Subscription {
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
expiresAt DateTime
|
expiresAt DateTime
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AccountType,
|
AccountType,
|
||||||
Currency,
|
Currency,
|
||||||
|
DataSource,
|
||||||
PrismaClient,
|
PrismaClient,
|
||||||
Role,
|
Role,
|
||||||
Type
|
Type
|
||||||
@ -135,17 +136,47 @@ async function main() {
|
|||||||
where: { id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f' }
|
where: { id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await prisma.symbolProfile.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
countries: [{ code: 'US', weight: 1 }],
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
|
id: '2bd26362-136e-411c-b578-334084b4cdcc',
|
||||||
|
symbol: 'AMZN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
countries: [{ code: 'US', weight: 1 }],
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
|
id: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e',
|
||||||
|
symbol: 'TSLA'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
countries: [
|
||||||
|
{ code: 'US', weight: 0.9886789999999981 },
|
||||||
|
{ code: 'NL', weight: 0.000203 },
|
||||||
|
{ code: 'CA', weight: 0.000362 }
|
||||||
|
],
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
|
id: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||||
|
symbol: 'VTI'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
skipDuplicates: true
|
||||||
|
});
|
||||||
|
|
||||||
await prisma.order.createMany({
|
await prisma.order.createMany({
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094',
|
accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
||||||
fee: 30,
|
fee: 30,
|
||||||
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
||||||
quantity: 50,
|
quantity: 50,
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
|
symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 42.97,
|
unitPrice: 42.97,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -154,6 +185,7 @@ async function main() {
|
|||||||
accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
|
accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
||||||
fee: 29.9,
|
fee: 29.9,
|
||||||
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
||||||
@ -167,11 +199,13 @@ async function main() {
|
|||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
||||||
fee: 80.79,
|
fee: 80.79,
|
||||||
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
||||||
quantity: 5,
|
quantity: 5,
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
|
symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 2021.99,
|
unitPrice: 2021.99,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -180,11 +214,13 @@ async function main() {
|
|||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 144.38,
|
unitPrice: 144.38,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -193,11 +229,13 @@ async function main() {
|
|||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 147.99,
|
unitPrice: 147.99,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -206,11 +244,13 @@ async function main() {
|
|||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 151.41,
|
unitPrice: 151.41,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -219,11 +259,13 @@ async function main() {
|
|||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 177.69,
|
unitPrice: 177.69,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -232,11 +274,13 @@ async function main() {
|
|||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 203.15,
|
unitPrice: 203.15,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
|
@ -4698,6 +4698,11 @@ cosmiconfig@^7.0.0:
|
|||||||
path-type "^4.0.0"
|
path-type "^4.0.0"
|
||||||
yaml "^1.10.0"
|
yaml "^1.10.0"
|
||||||
|
|
||||||
|
countries-list@2.6.1:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/countries-list/-/countries-list-2.6.1.tgz#d479757ac873b1e596ccea0a925962d20396c0cb"
|
||||||
|
integrity sha512-jXM1Nv3U56dPQ1DsUSsEaGmLHburo4fnB7m+1yhWDUVvx5gXCd1ok/y3gXCjXzhqyawG+igcPYcAl4qjkvopaQ==
|
||||||
|
|
||||||
countup.js@2.0.7:
|
countup.js@2.0.7:
|
||||||
version "2.0.7"
|
version "2.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/countup.js/-/countup.js-2.0.7.tgz#56b72a87fc0ee3cadb38356c246ccac88fb0a8cc"
|
resolved "https://registry.yarnpkg.com/countup.js/-/countup.js-2.0.7.tgz#56b72a87fc0ee3cadb38356c246ccac88fb0a8cc"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user