Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
e68ebdf76d | |||
949cf58eef | |||
0816defb95 | |||
a076a1c933 | |||
40c95a541d | |||
2d04d7b8d5 | |||
94e0feac68 | |||
cd9e974c40 | |||
11700f75d9 | |||
0439a7beaa | |||
608b195ba9 | |||
8cb5fd64dd | |||
ef317a86ed | |||
19ada83d0b | |||
6ecf66ea2a | |||
65ba16c07c |
42
CHANGELOG.md
42
CHANGELOG.md
@ -5,6 +5,48 @@ 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).
|
||||
|
||||
## 0.96.0 - 30.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the absolute change to the position detail dialog
|
||||
- Added the number of transactions to the position detail dialog
|
||||
|
||||
### Changed
|
||||
|
||||
- Harmonized the slogan to "Open Source Portfolio Tracker"
|
||||
|
||||
## 0.95.0 - 28.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a data source attribute to the transactions model
|
||||
|
||||
## 0.94.0 - 27.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the generic scraper symbols to the symbol lookup results
|
||||
|
||||
## 0.93.0 - 26.04.2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the user table styling of the admin control panel
|
||||
- Improved the background colors in the dark mode
|
||||
|
||||
## 0.92.0 - 25.04.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Prepared further for multi accounts support: store account for new transactions
|
||||
- Added a horizontal scrollbar to the user table of the admin control panel
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the header with outdated data
|
||||
- Fixed an issue on the about page with outdated data
|
||||
|
||||
## 0.91.0 - 25.04.2021
|
||||
|
||||
### Added
|
||||
|
@ -97,7 +97,7 @@ export class AdminService {
|
||||
},
|
||||
select: {
|
||||
_count: {
|
||||
select: { Order: true }
|
||||
select: { Account: true, Order: true }
|
||||
},
|
||||
alias: true,
|
||||
Analytics: {
|
||||
|
@ -36,7 +36,10 @@ export class ExperimentalService {
|
||||
const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => {
|
||||
return {
|
||||
...order,
|
||||
accountId: undefined,
|
||||
accountUserId: undefined,
|
||||
createdAt: new Date(),
|
||||
dataSource: undefined,
|
||||
date: parseISO(order.date),
|
||||
fee: 0,
|
||||
id: undefined,
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { Settings, User } from '@prisma/client';
|
||||
import { Account, Settings, User } from '@prisma/client';
|
||||
|
||||
export type UserWithSettings = User & { Settings: Settings };
|
||||
export type UserWithSettings = User & {
|
||||
Account: Account[];
|
||||
Settings: Settings;
|
||||
};
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { Currency, Type } from '@prisma/client';
|
||||
import { Currency, DataSource, Type } from '@prisma/client';
|
||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||
|
||||
export class CreateOrderDto {
|
||||
@IsString()
|
||||
accountId: string;
|
||||
|
||||
@IsString()
|
||||
currency: Currency;
|
||||
|
||||
@IsString()
|
||||
dataSource: DataSource;
|
||||
|
||||
@IsISO8601()
|
||||
date: string;
|
||||
|
||||
|
@ -118,6 +118,9 @@ export class OrderController {
|
||||
|
||||
const date = parseISO(data.date);
|
||||
|
||||
const accountId = data.accountId;
|
||||
delete data.accountId;
|
||||
|
||||
if (data.platformId) {
|
||||
const platformId = data.platformId;
|
||||
delete data.platformId;
|
||||
@ -126,6 +129,11 @@ export class OrderController {
|
||||
{
|
||||
...data,
|
||||
date,
|
||||
Account: {
|
||||
connect: {
|
||||
id_userId: { id: accountId, userId: this.request.user.id }
|
||||
}
|
||||
},
|
||||
Platform: { connect: { id: platformId } },
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
@ -138,6 +146,11 @@ export class OrderController {
|
||||
{
|
||||
...data,
|
||||
date,
|
||||
Account: {
|
||||
connect: {
|
||||
id_userId: { id: accountId, userId: this.request.user.id }
|
||||
}
|
||||
},
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
this.request.user.id
|
||||
@ -169,6 +182,9 @@ export class OrderController {
|
||||
|
||||
const date = parseISO(data.date);
|
||||
|
||||
const accountId = data.accountId;
|
||||
delete data.accountId;
|
||||
|
||||
if (data.platformId) {
|
||||
const platformId = data.platformId;
|
||||
delete data.platformId;
|
||||
@ -178,6 +194,11 @@ export class OrderController {
|
||||
data: {
|
||||
...data,
|
||||
date,
|
||||
Account: {
|
||||
connect: {
|
||||
id_userId: { id: accountId, userId: this.request.user.id }
|
||||
}
|
||||
},
|
||||
Platform: { connect: { id: platformId } },
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
},
|
||||
@ -199,6 +220,11 @@ export class OrderController {
|
||||
data: {
|
||||
...data,
|
||||
date,
|
||||
Account: {
|
||||
connect: {
|
||||
id_userId: { id: accountId, userId: this.request.user.id }
|
||||
}
|
||||
},
|
||||
Platform: originalOrder.platformId
|
||||
? { disconnect: true }
|
||||
: undefined,
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { Currency, Type } from '@prisma/client';
|
||||
import { Currency, DataSource, Type } from '@prisma/client';
|
||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||
|
||||
export class UpdateOrderDto {
|
||||
@IsString()
|
||||
accountId: string;
|
||||
|
||||
@IsString()
|
||||
currency: Currency;
|
||||
|
||||
@IsString()
|
||||
dataSource: DataSource;
|
||||
|
||||
@IsISO8601()
|
||||
date: string;
|
||||
|
||||
|
@ -16,4 +16,5 @@ export interface Position {
|
||||
investmentInOriginalCurrency?: number;
|
||||
marketPrice?: number;
|
||||
quantity: number;
|
||||
transactionCount: number;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface PortfolioPositionDetail {
|
||||
averagePrice: number;
|
||||
currency: string;
|
||||
currency: Currency;
|
||||
firstBuyDate: string;
|
||||
grossPerformance: number;
|
||||
grossPerformancePercent: number;
|
||||
@ -11,6 +13,7 @@ export interface PortfolioPositionDetail {
|
||||
minPrice: number;
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
transactionCount: number;
|
||||
}
|
||||
|
||||
export interface HistoricalDataItem {
|
||||
|
@ -20,6 +20,7 @@ export interface PortfolioPosition {
|
||||
sector?: string;
|
||||
shareCurrent: number;
|
||||
shareInvestment: number;
|
||||
transactionCount: number;
|
||||
symbol: string;
|
||||
type?: string;
|
||||
url?: string;
|
||||
|
@ -209,7 +209,8 @@ export class PortfolioService {
|
||||
firstBuyDate,
|
||||
investment,
|
||||
marketPrice,
|
||||
quantity
|
||||
quantity,
|
||||
transactionCount
|
||||
} = portfolio.getPositions(new Date())[aSymbol];
|
||||
|
||||
const historicalData = await this.dataProviderService.getHistorical(
|
||||
@ -262,6 +263,7 @@ export class PortfolioService {
|
||||
maxPrice,
|
||||
minPrice,
|
||||
quantity,
|
||||
transactionCount,
|
||||
grossPerformance: this.exchangeRateDataService.toCurrency(
|
||||
marketPrice - averagePrice,
|
||||
currency,
|
||||
@ -315,7 +317,8 @@ export class PortfolioService {
|
||||
maxPrice: undefined,
|
||||
minPrice: undefined,
|
||||
quantity: undefined,
|
||||
symbol: aSymbol
|
||||
symbol: aSymbol,
|
||||
transactionCount: undefined
|
||||
};
|
||||
}
|
||||
|
||||
@ -331,7 +334,8 @@ export class PortfolioService {
|
||||
maxPrice: undefined,
|
||||
minPrice: undefined,
|
||||
quantity: undefined,
|
||||
symbol: aSymbol
|
||||
symbol: aSymbol,
|
||||
transactionCount: undefined
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Currency, DataSource } from '@prisma/client';
|
||||
|
||||
export interface SymbolItem {
|
||||
currency: Currency;
|
||||
dataSource: DataSource;
|
||||
marketPrice: number;
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Currency } from '@prisma/client';
|
||||
@ -10,31 +12,51 @@ import { SymbolItem } from './interfaces/symbol-item.interface';
|
||||
@Injectable()
|
||||
export class SymbolService {
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService
|
||||
) {}
|
||||
|
||||
public async get(aSymbol: string): Promise<SymbolItem> {
|
||||
const response = await this.dataProviderService.get([aSymbol]);
|
||||
const { currency, marketPrice } = response[aSymbol];
|
||||
const { currency, dataSource, marketPrice } = response[aSymbol];
|
||||
|
||||
return {
|
||||
dataSource,
|
||||
marketPrice,
|
||||
currency: <Currency>(<unknown>currency)
|
||||
};
|
||||
}
|
||||
|
||||
public async lookup(aQuery: string): Promise<LookupItem[]> {
|
||||
public async lookup(aQuery = ''): Promise<LookupItem[]> {
|
||||
const query = aQuery.toLowerCase();
|
||||
const results: LookupItem[] = [];
|
||||
|
||||
if (!query) {
|
||||
return results;
|
||||
}
|
||||
|
||||
const get = bent(
|
||||
`https://query1.finance.yahoo.com/v1/finance/search?q=${aQuery}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||
`https://query1.finance.yahoo.com/v1/finance/search?q=${query}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||
'GET',
|
||||
'json',
|
||||
200
|
||||
);
|
||||
|
||||
// Add custom symbols
|
||||
const scraperConfigurations = await this.ghostfolioScraperApiService.getScraperConfigurations();
|
||||
scraperConfigurations.forEach((scraperConfiguration) => {
|
||||
if (scraperConfiguration.name.toLowerCase().startsWith(query)) {
|
||||
results.push({
|
||||
name: scraperConfiguration.name,
|
||||
symbol: scraperConfiguration.symbol
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const { quotes } = await get();
|
||||
|
||||
return quotes
|
||||
const searchResult = quotes
|
||||
.filter(({ isYahooFinance }) => {
|
||||
return isYahooFinance;
|
||||
})
|
||||
@ -59,6 +81,8 @@ export class SymbolService {
|
||||
symbol: convertFromYahooSymbol(symbol)
|
||||
};
|
||||
});
|
||||
|
||||
return results.concat(searchResult);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Account, Currency } from '@prisma/client';
|
||||
|
||||
import { Access } from './access.interface';
|
||||
|
||||
export interface User {
|
||||
access: Access[];
|
||||
accounts: Account[];
|
||||
alias?: string;
|
||||
id: string;
|
||||
permissions: string[];
|
||||
|
@ -25,6 +25,7 @@ export class UserService {
|
||||
) {}
|
||||
|
||||
public async getUser({
|
||||
Account,
|
||||
alias,
|
||||
id,
|
||||
role,
|
||||
@ -53,6 +54,7 @@ export class UserService {
|
||||
id: accessItem.id
|
||||
};
|
||||
}),
|
||||
accounts: Account,
|
||||
permissions: currentPermissions,
|
||||
settings: {
|
||||
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
|
||||
@ -69,7 +71,7 @@ export class UserService {
|
||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||
): Promise<UserWithSettings | null> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
include: { Settings: true },
|
||||
include: { Account: true, Settings: true },
|
||||
where: userWhereUniqueInput
|
||||
});
|
||||
|
||||
|
@ -14,6 +14,9 @@ import { PrismaService } from '../services/prisma.service';
|
||||
import { RulesService } from '../services/rules.service';
|
||||
import { Portfolio } from './portfolio';
|
||||
|
||||
const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269';
|
||||
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
|
||||
|
||||
describe('Portfolio', () => {
|
||||
let alphaVantageService: AlphaVantageService;
|
||||
let configurationService: ConfigurationService;
|
||||
@ -69,13 +72,13 @@ describe('Portfolio', () => {
|
||||
accessToken: null,
|
||||
alias: 'Test',
|
||||
createdAt: new Date(),
|
||||
id: '',
|
||||
id: USER_ID,
|
||||
provider: null,
|
||||
role: Role.USER,
|
||||
Settings: {
|
||||
currency: Currency.CHF,
|
||||
updatedAt: new Date(),
|
||||
userId: ''
|
||||
userId: USER_ID
|
||||
},
|
||||
thirdPartyId: null,
|
||||
updatedAt: new Date()
|
||||
@ -126,6 +129,8 @@ describe('Portfolio', () => {
|
||||
it('should return ["BTC"]', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
fee: 0,
|
||||
@ -137,7 +142,7 @@ describe('Portfolio', () => {
|
||||
type: Type.BUY,
|
||||
unitPrice: 49631.24,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
@ -224,6 +229,8 @@ describe('Portfolio', () => {
|
||||
it('should return ["ETHUSD"]', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
fee: 0,
|
||||
@ -235,7 +242,7 @@ describe('Portfolio', () => {
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
@ -276,7 +283,7 @@ describe('Portfolio', () => {
|
||||
}
|
||||
},
|
||||
quantity: 0.2,
|
||||
shareCurrent: 1,
|
||||
// shareCurrent: 1,
|
||||
shareInvestment: 1,
|
||||
symbol: 'ETHUSD',
|
||||
type: 'Cryptocurrency'
|
||||
@ -316,6 +323,8 @@ describe('Portfolio', () => {
|
||||
it('should return ["ETHUSD"]', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
fee: 0,
|
||||
@ -327,9 +336,11 @@ describe('Portfolio', () => {
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
},
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
fee: 0,
|
||||
@ -341,7 +352,7 @@ describe('Portfolio', () => {
|
||||
type: Type.BUY,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
@ -388,6 +399,8 @@ describe('Portfolio', () => {
|
||||
it('should return ["BTCUSD", "ETHUSD"]', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.EUR,
|
||||
date: new Date(getUtc('2017-08-16')),
|
||||
@ -399,9 +412,11 @@ describe('Portfolio', () => {
|
||||
type: Type.BUY,
|
||||
unitPrice: 3562.089535970158,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
},
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
fee: 2.99,
|
||||
@ -413,7 +428,7 @@ describe('Portfolio', () => {
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
@ -473,6 +488,8 @@ describe('Portfolio', () => {
|
||||
it('should work with buy and sell', async () => {
|
||||
await portfolio.setOrders([
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
fee: 1.0,
|
||||
@ -484,9 +501,11 @@ describe('Portfolio', () => {
|
||||
type: Type.BUY,
|
||||
unitPrice: 991.49,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
},
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
fee: 1.0,
|
||||
@ -498,9 +517,11 @@ describe('Portfolio', () => {
|
||||
type: Type.SELL,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
},
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
accountUserId: USER_ID,
|
||||
createdAt: null,
|
||||
currency: Currency.USD,
|
||||
fee: 1.0,
|
||||
@ -512,7 +533,7 @@ describe('Portfolio', () => {
|
||||
type: Type.BUY,
|
||||
unitPrice: 1050,
|
||||
updatedAt: null,
|
||||
userId: null
|
||||
userId: USER_ID
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -57,7 +57,7 @@ export class Portfolio implements PortfolioInterface {
|
||||
public async addCurrentPortfolioItems() {
|
||||
const currentData = await this.dataProviderService.get(this.getSymbols());
|
||||
|
||||
let currentDate = new Date();
|
||||
const currentDate = new Date();
|
||||
|
||||
const year = getYear(currentDate);
|
||||
const month = getMonth(currentDate);
|
||||
@ -82,7 +82,9 @@ export class Portfolio implements PortfolioInterface {
|
||||
marketPrice:
|
||||
currentData[symbol]?.marketPrice ??
|
||||
portfolioItemsYesterday.positions[symbol]?.marketPrice,
|
||||
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity
|
||||
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity,
|
||||
transactionCount:
|
||||
portfolioItemsYesterday?.positions[symbol]?.transactionCount
|
||||
};
|
||||
});
|
||||
|
||||
@ -289,7 +291,9 @@ export class Portfolio implements PortfolioInterface {
|
||||
data[symbol]?.currency,
|
||||
this.user.Settings.currency
|
||||
) / value,
|
||||
shareInvestment: portfolioItem.positions[symbol].investment / investment
|
||||
shareInvestment:
|
||||
portfolioItem.positions[symbol].investment / investment,
|
||||
transactionCount: portfolioItem.positions[symbol].transactionCount
|
||||
};
|
||||
});
|
||||
|
||||
@ -582,7 +586,8 @@ export class Portfolio implements PortfolioInterface {
|
||||
marketPrice:
|
||||
historicalData[symbol]?.[format(currentDate, 'yyyy-MM-dd')]
|
||||
?.marketPrice || 0,
|
||||
quantity: 0
|
||||
quantity: 0,
|
||||
transactionCount: 0
|
||||
};
|
||||
});
|
||||
|
||||
@ -623,7 +628,8 @@ export class Portfolio implements PortfolioInterface {
|
||||
marketPrice:
|
||||
historicalData[symbol]?.[format(yesterday, 'yyyy-MM-dd')]
|
||||
?.marketPrice || 0,
|
||||
quantity: 0
|
||||
quantity: 0,
|
||||
transactionCount: 0
|
||||
};
|
||||
});
|
||||
|
||||
@ -730,6 +736,10 @@ export class Portfolio implements PortfolioInterface {
|
||||
order.getSymbol()
|
||||
].currency = order.getCurrency();
|
||||
|
||||
this.portfolioItems[i].positions[
|
||||
order.getSymbol()
|
||||
].transactionCount += 1;
|
||||
|
||||
if (order.getType() === 'BUY') {
|
||||
if (
|
||||
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DataProviderService } from './data-provider.service';
|
||||
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Injectable()
|
||||
@ -25,6 +26,7 @@ export class DataGatheringService {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
||||
private prisma: PrismaService
|
||||
) {}
|
||||
|
||||
@ -183,6 +185,17 @@ export class DataGatheringService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getCustomSymbolsToGather(startDate?: Date) {
|
||||
const scraperConfigurations = await this.ghostfolioScraperApi.getScraperConfigurations();
|
||||
|
||||
return scraperConfigurations.map((scraperConfiguration) => {
|
||||
return {
|
||||
date: startDate,
|
||||
symbol: scraperConfiguration.symbol
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getBenchmarksToGather(startDate: Date) {
|
||||
const benchmarksToGather = benchmarks.map((symbol) => {
|
||||
return {
|
||||
@ -201,32 +214,6 @@ export class DataGatheringService {
|
||||
return benchmarksToGather;
|
||||
}
|
||||
|
||||
private async getCustomSymbolsToGather(startDate: Date) {
|
||||
const customSymbolsToGather = [];
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_CUSTOM_SYMBOLS')) {
|
||||
try {
|
||||
const {
|
||||
value: scraperConfigString
|
||||
} = await this.prisma.property.findFirst({
|
||||
select: {
|
||||
value: true
|
||||
},
|
||||
where: { key: 'SCRAPER_CONFIG' }
|
||||
});
|
||||
|
||||
JSON.parse(scraperConfigString).forEach((item) => {
|
||||
customSymbolsToGather.push({
|
||||
date: startDate,
|
||||
symbol: item.symbol
|
||||
});
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return customSymbolsToGather;
|
||||
}
|
||||
|
||||
private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> {
|
||||
const startDate = subDays(resetHours(new Date()), 7);
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DataSource } from '.prisma/client';
|
||||
import { getYesterday } from '@ghostfolio/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as bent from 'bent';
|
||||
@ -12,6 +13,7 @@ import {
|
||||
MarketState
|
||||
} from '../../interfaces/interfaces';
|
||||
import { PrismaService } from '../../prisma.service';
|
||||
import { ScraperConfig } from './interfaces/scraper-config.interface';
|
||||
|
||||
@Injectable()
|
||||
export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
@ -29,7 +31,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
try {
|
||||
const symbol = aSymbols[0];
|
||||
|
||||
const scraperConfig = await this.getScraperConfig(symbol);
|
||||
const scraperConfig = await this.getScraperConfigurationBySymbol(symbol);
|
||||
|
||||
const { marketPrice } = await this.prisma.marketData.findFirst({
|
||||
orderBy: {
|
||||
@ -44,6 +46,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
[symbol]: {
|
||||
marketPrice,
|
||||
currency: scraperConfig?.currency,
|
||||
dataSource: DataSource.GHOSTFOLIO,
|
||||
marketState: MarketState.delayed,
|
||||
name: scraperConfig?.name
|
||||
}
|
||||
@ -70,15 +73,17 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
try {
|
||||
const symbol = aSymbols[0];
|
||||
|
||||
const scraperConfig = await this.getScraperConfig(symbol);
|
||||
const scraperConfiguration = await this.getScraperConfigurationBySymbol(
|
||||
symbol
|
||||
);
|
||||
|
||||
const get = bent(scraperConfig?.url, 'GET', 'string', 200, {});
|
||||
const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {});
|
||||
|
||||
const html = await get();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const value = this.extractNumberFromString(
|
||||
$(scraperConfig?.selector).text()
|
||||
$(scraperConfiguration?.selector).text()
|
||||
);
|
||||
|
||||
return {
|
||||
@ -95,6 +100,23 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
public async getScraperConfigurations(): Promise<ScraperConfig[]> {
|
||||
try {
|
||||
const {
|
||||
value: scraperConfigString
|
||||
} = await this.prisma.property.findFirst({
|
||||
select: {
|
||||
value: true
|
||||
},
|
||||
where: { key: 'SCRAPER_CONFIG' }
|
||||
});
|
||||
|
||||
return JSON.parse(scraperConfigString);
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private extractNumberFromString(aString: string): number {
|
||||
try {
|
||||
const [numberString] = aString.match(
|
||||
@ -106,22 +128,10 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private async getScraperConfig(aSymbol: string) {
|
||||
try {
|
||||
const {
|
||||
value: scraperConfigString
|
||||
} = await this.prisma.property.findFirst({
|
||||
select: {
|
||||
value: true
|
||||
},
|
||||
where: { key: 'SCRAPER_CONFIG' }
|
||||
});
|
||||
|
||||
return JSON.parse(scraperConfigString).find((item) => {
|
||||
return item.symbol === aSymbol;
|
||||
});
|
||||
} catch {}
|
||||
|
||||
return {};
|
||||
private async getScraperConfigurationBySymbol(aSymbol: string) {
|
||||
const scraperConfigurations = await this.getScraperConfigurations();
|
||||
return scraperConfigurations.find((scraperConfiguration) => {
|
||||
return scraperConfiguration.symbol === aSymbol;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { Currency } from '.prisma/client';
|
||||
|
||||
export interface ScraperConfig {
|
||||
currency: Currency;
|
||||
name: string;
|
||||
selector: string;
|
||||
symbol: string;
|
||||
url: string;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { DataSource } from '.prisma/client';
|
||||
import { getToday, getYesterday } from '@ghostfolio/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as bent from 'bent';
|
||||
@ -39,6 +40,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
return {
|
||||
'GF.FEAR_AND_GREED_INDEX': {
|
||||
currency: undefined,
|
||||
dataSource: DataSource.RAKUTEN,
|
||||
marketPrice: fgi.now.value,
|
||||
marketState: MarketState.open,
|
||||
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DataSource } from '.prisma/client';
|
||||
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { format } from 'date-fns';
|
||||
@ -49,6 +50,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
response[symbol] = {
|
||||
currency: parseCurrency(value.price?.currency),
|
||||
dataSource: DataSource.YAHOO,
|
||||
exchange: this.parseExchange(value.price?.exchangeName),
|
||||
marketState:
|
||||
value.price?.marketState === 'REGULAR' || isCrypto(symbol)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Currency, Platform } from '@prisma/client';
|
||||
import { Currency, DataSource, Platform } from '@prisma/client';
|
||||
|
||||
import { OrderType } from '../../models/order-type';
|
||||
|
||||
@ -51,6 +51,7 @@ export interface IDataProviderHistoricalResponse {
|
||||
|
||||
export interface IDataProviderResponse {
|
||||
currency: Currency;
|
||||
dataSource: DataSource;
|
||||
exchange?: string;
|
||||
industry?: Industry;
|
||||
marketChange?: number;
|
||||
|
@ -150,7 +150,7 @@
|
||||
>Account</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionForAdminControl"
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
class="d-block d-sm-none"
|
||||
[routerLink]="['/admin']"
|
||||
i18n
|
||||
|
@ -34,16 +34,16 @@
|
||||
<div *ngIf="showDetails" class="row">
|
||||
<div class="d-flex col justify-content-end">
|
||||
<gf-value
|
||||
colorizeSign="true"
|
||||
isCurrency="true"
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div class="col">
|
||||
<gf-value
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||
|
@ -16,17 +16,17 @@
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end mb-2"
|
||||
colorizeSign="true"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[currency]="baseCurrency"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
|
||||
></gf-value>
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentGrossPerformancePercent
|
||||
@ -39,17 +39,17 @@
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end mb-2"
|
||||
colorizeSign="true"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[currency]="baseCurrency"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||
></gf-value>
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||
|
@ -23,6 +23,7 @@ export class PositionDetailDialog {
|
||||
public benchmarkDataItems: LineChartItem[];
|
||||
public currency: string;
|
||||
public firstBuyDate: string;
|
||||
public grossPerformance: number;
|
||||
public grossPerformancePercent: number;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public investment: number;
|
||||
@ -30,6 +31,7 @@ export class PositionDetailDialog {
|
||||
public maxPrice: number;
|
||||
public minPrice: number;
|
||||
public quantity: number;
|
||||
public transactionCount: number;
|
||||
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
@ -44,18 +46,21 @@ export class PositionDetailDialog {
|
||||
averagePrice,
|
||||
currency,
|
||||
firstBuyDate,
|
||||
grossPerformance,
|
||||
grossPerformancePercent,
|
||||
historicalData,
|
||||
investment,
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
quantity
|
||||
quantity,
|
||||
transactionCount
|
||||
}) => {
|
||||
this.averagePrice = averagePrice;
|
||||
this.benchmarkDataItems = [];
|
||||
this.currency = currency;
|
||||
this.firstBuyDate = firstBuyDate;
|
||||
this.grossPerformance = quantity * grossPerformance;
|
||||
this.grossPerformancePercent = grossPerformancePercent;
|
||||
this.historicalDataItems = historicalData.map(
|
||||
(historicalDataItem) => {
|
||||
@ -75,6 +80,7 @@ export class PositionDetailDialog {
|
||||
this.maxPrice = maxPrice;
|
||||
this.minPrice = minPrice;
|
||||
this.quantity = quantity;
|
||||
this.transactionCount = transactionCount;
|
||||
|
||||
if (isToday(parseISO(this.firstBuyDate))) {
|
||||
// Add average price
|
||||
|
@ -21,19 +21,22 @@
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
label="Performance"
|
||||
label="Change"
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[currency]="data.baseCurrency"
|
||||
[locale]="data.locale"
|
||||
[value]="grossPerformancePercent"
|
||||
[value]="grossPerformance"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="First Buy Date"
|
||||
label="Performance"
|
||||
size="medium"
|
||||
[value]="firstBuyDate"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="grossPerformancePercent"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
@ -76,9 +79,9 @@
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
isCurrency="true"
|
||||
label="Quantity"
|
||||
size="medium"
|
||||
[isCurrency]="true"
|
||||
[value]="quantity"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -91,6 +94,22 @@
|
||||
[value]="investment"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="First Buy Date"
|
||||
size="medium"
|
||||
[value]="firstBuyDate"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Transactions"
|
||||
size="medium"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="transactionCount"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,14 +42,14 @@
|
||||
<div class="d-flex mt-1">
|
||||
<gf-value
|
||||
class="mr-3"
|
||||
colorizeSign="true"
|
||||
[colorizeSign]="true"
|
||||
[currency]="position?.currency"
|
||||
[locale]="locale"
|
||||
[value]="position?.grossPerformance"
|
||||
></gf-value>
|
||||
<gf-value
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="position?.grossPerformancePercent"
|
||||
></gf-value>
|
||||
|
@ -28,8 +28,8 @@
|
||||
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
colorizeSign="true"
|
||||
isPercent="true"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.grossPerformancePercent"
|
||||
></gf-value>
|
||||
@ -50,7 +50,7 @@
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
isPercent="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.shareInvestment"
|
||||
></gf-value>
|
||||
@ -71,7 +71,7 @@
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
isPercent="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.shareCurrent"
|
||||
></gf-value>
|
||||
|
@ -116,7 +116,7 @@
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
isCurrency="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.quantity"
|
||||
></gf-value>
|
||||
@ -137,7 +137,7 @@
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
isCurrency="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.unitPrice"
|
||||
></gf-value>
|
||||
@ -158,7 +158,7 @@
|
||||
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
isCurrency="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.fee"
|
||||
></gf-value>
|
||||
|
@ -42,6 +42,8 @@ export class AboutPageComponent implements OnInit {
|
||||
info.globalPermissions,
|
||||
permissions.enableSubscription
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.isLoggedIn = !!this.tokenStorageService.getToken();
|
||||
|
@ -70,42 +70,36 @@
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Users</h3>
|
||||
<mat-card class="px-0">
|
||||
<mat-card-content>
|
||||
<table class="users w-100">
|
||||
<mat-card-content class="users">
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="mat-header-row">
|
||||
<th class="mat-header-cell pl-2 py-2 text-truncate" i18n>
|
||||
User
|
||||
</th>
|
||||
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
|
||||
<th class="mat-header-cell pl-3 py-2" i18n>User</th>
|
||||
<th class="mat-header-cell pr-2 py-2" i18n>
|
||||
Registration Date
|
||||
</th>
|
||||
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
|
||||
Transactions
|
||||
</th>
|
||||
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
|
||||
Engagement
|
||||
</th>
|
||||
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
|
||||
Last Activitiy
|
||||
</th>
|
||||
<th class="mat-header-cell pr-2 py-2" i18n>Accounts</th>
|
||||
<th class="mat-header-cell pr-2 py-2" i18n>Transactions</th>
|
||||
<th class="mat-header-cell pr-2 py-2" i18n>Engagement</th>
|
||||
<th class="mat-header-cell pr-3 py-2" i18n>Last Activitiy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let userItem of users" class="mat-row">
|
||||
<td class="mat-cell pl-2 py-2 text-truncate">
|
||||
<td class="mat-cell pl-3 py-2">
|
||||
{{ userItem.alias || userItem.id }}
|
||||
</td>
|
||||
<td class="mat-cell pr-2 py-2 text-truncate">
|
||||
<td class="mat-cell pr-2 py-2">
|
||||
{{ userItem.createdAt | date: defaultDateFormat }}
|
||||
</td>
|
||||
<td class="mat-cell pr-2 py-2 text-truncate">
|
||||
{{ userItem._count?.Order }}
|
||||
<td class="mat-cell pr-2 py-2">
|
||||
{{ userItem._count?.Account }}
|
||||
</td>
|
||||
<td class="mat-cell pr-2 py-2 text-truncate">
|
||||
<td class="mat-cell pr-2 py-2">{{ userItem._count?.Order }}</td>
|
||||
<td class="mat-cell pr-2 py-2">
|
||||
{{ userItem.Analytics?.activityCount }}
|
||||
</td>
|
||||
<td class="mat-cell pr-2 py-2 text-truncate">
|
||||
<td class="mat-cell pr-3 py-2">
|
||||
{{ formatDistanceToNow(userItem.Analytics?.updatedAt) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -2,16 +2,25 @@
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
table {
|
||||
.mat-card-content {
|
||||
&.users {
|
||||
table-layout: fixed;
|
||||
overflow-x: auto;
|
||||
|
||||
tr {
|
||||
&:nth-child(even) {
|
||||
background-color: rgba(
|
||||
var(--dark-primary-text),
|
||||
var(--palette-background-hover-alpha)
|
||||
);
|
||||
table {
|
||||
min-width: 100%;
|
||||
|
||||
tr {
|
||||
&:nth-child(even) {
|
||||
background-color: rgba(
|
||||
var(--dark-primary-text),
|
||||
var(--palette-background-hover-alpha)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-row,
|
||||
.mat-header-row {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { FormControl, Validators } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { Currency, Order as OrderModel } from '@prisma/client';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import {
|
||||
debounceTime,
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'h-100' },
|
||||
@ -33,7 +34,7 @@ export class CreateOrUpdateTransactionDialog {
|
||||
public isLoading = false;
|
||||
public platforms: { id: string; name: string }[];
|
||||
public searchSymbolCtrl = new FormControl(
|
||||
this.data.symbol,
|
||||
this.data.transaction.symbol,
|
||||
Validators.required
|
||||
);
|
||||
|
||||
@ -43,7 +44,7 @@ export class CreateOrUpdateTransactionDialog {
|
||||
private cd: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: OrderModel
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@ -72,14 +73,15 @@ export class CreateOrUpdateTransactionDialog {
|
||||
|
||||
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
||||
this.isLoading = true;
|
||||
this.data.symbol = event.option.value;
|
||||
this.data.transaction.symbol = event.option.value;
|
||||
|
||||
this.dataService
|
||||
.fetchSymbolItem(this.data.symbol)
|
||||
.fetchSymbolItem(this.data.transaction.symbol)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ currency, marketPrice }) => {
|
||||
this.data.currency = currency;
|
||||
this.data.unitPrice = marketPrice;
|
||||
.subscribe(({ currency, dataSource, marketPrice }) => {
|
||||
this.data.transaction.currency = currency;
|
||||
this.data.transaction.dataSource = dataSource;
|
||||
this.data.transaction.unitPrice = marketPrice;
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
@ -88,10 +90,11 @@ export class CreateOrUpdateTransactionDialog {
|
||||
}
|
||||
|
||||
public onUpdateSymbolByTyping(value: string) {
|
||||
this.data.currency = null;
|
||||
this.data.unitPrice = null;
|
||||
this.data.transaction.currency = null;
|
||||
this.data.transaction.dataSource = null;
|
||||
this.data.transaction.unitPrice = null;
|
||||
|
||||
this.data.symbol = value;
|
||||
this.data.transaction.symbol = value;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
|
||||
<h1 *ngIf="data.id" mat-dialog-title i18n>Update transaction</h1>
|
||||
<h1 *ngIf="!data.id" mat-dialog-title i18n>Add transaction</h1>
|
||||
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
|
||||
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
@ -25,7 +25,7 @@
|
||||
class="autocomplete"
|
||||
[value]="lookupItem.symbol"
|
||||
>
|
||||
<span class="mr-2 symbol">{{ lookupItem.symbol }}</span
|
||||
<span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span
|
||||
><span><b>{{ lookupItem.name }}</b></span>
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
@ -36,7 +36,7 @@
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Type</mat-label>
|
||||
<mat-select name="type" required [(value)]="data.type">
|
||||
<mat-select name="type" required [(value)]="data.transaction.type">
|
||||
<mat-option value="BUY" i18n> BUY </mat-option>
|
||||
<mat-option value="SELL" i18n> SELL </mat-option>
|
||||
</mat-select>
|
||||
@ -50,7 +50,7 @@
|
||||
disabled
|
||||
name="currency"
|
||||
required
|
||||
[(value)]="data.currency"
|
||||
[(value)]="data.transaction.currency"
|
||||
>
|
||||
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
||||
>{{ currency }}</mat-option
|
||||
@ -58,6 +58,18 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="d-none">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Data Source</mat-label>
|
||||
<input
|
||||
disabled
|
||||
matInput
|
||||
name="dataSource"
|
||||
required
|
||||
[(ngModel)]="data.transaction.dataSource"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Date</mat-label>
|
||||
@ -67,7 +79,7 @@
|
||||
name="date"
|
||||
required
|
||||
[matDatepicker]="date"
|
||||
[(ngModel)]="data.date"
|
||||
[(ngModel)]="data.transaction.date"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||
<mat-datepicker #date disabled="false"></mat-datepicker>
|
||||
@ -81,7 +93,7 @@
|
||||
name="fee"
|
||||
required
|
||||
type="number"
|
||||
[(ngModel)]="data.fee"
|
||||
[(ngModel)]="data.transaction.fee"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@ -93,7 +105,7 @@
|
||||
name="quantity"
|
||||
required
|
||||
type="number"
|
||||
[(ngModel)]="data.quantity"
|
||||
[(ngModel)]="data.transaction.quantity"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@ -105,14 +117,29 @@
|
||||
name="unitPrice"
|
||||
required
|
||||
type="number"
|
||||
[(ngModel)]="data.unitPrice"
|
||||
[(ngModel)]="data.transaction.unitPrice"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="d-none">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Account</mat-label>
|
||||
<mat-select
|
||||
disabled
|
||||
name="accountId"
|
||||
required
|
||||
[(value)]="data.transaction.accountId"
|
||||
>
|
||||
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
|
||||
>{{ account.name }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Platform</mat-label>
|
||||
<mat-select name="platformId" [(value)]="data.platformId">
|
||||
<mat-select name="platformId" [(value)]="data.transaction.platformId">
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<mat-option *ngFor="let platform of platforms" [value]="platform.id"
|
||||
>{{ platform.name }}</mat-option
|
||||
@ -127,7 +154,7 @@
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!(addTransactionForm.form.valid && data.symbol)"
|
||||
[disabled]="!(addTransactionForm.form.valid && data.transaction.symbol)"
|
||||
[mat-dialog-close]="data"
|
||||
>
|
||||
Save
|
||||
|
@ -9,6 +9,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
|
||||
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
|
||||
|
||||
@ -17,6 +18,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfSymbolModule,
|
||||
FormsModule,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { Account } from '@prisma/client';
|
||||
|
||||
import { Order } from '../../interfaces/order.interface';
|
||||
|
||||
export interface CreateOrUpdateTransactionDialogParams {
|
||||
accountId: string;
|
||||
accounts: Account[];
|
||||
transaction: Order;
|
||||
}
|
@ -1,8 +1,14 @@
|
||||
import { Currency, DataSource } from '.prisma/client';
|
||||
|
||||
export interface Order {
|
||||
currency: string;
|
||||
accountId: string;
|
||||
currency: Currency;
|
||||
dataSource: DataSource;
|
||||
date: Date;
|
||||
fee: number;
|
||||
id: string;
|
||||
quantity: number;
|
||||
platformId: string;
|
||||
symbol: string;
|
||||
type: string;
|
||||
unitPrice: number;
|
||||
|
@ -122,37 +122,10 @@ export class TransactionsPageComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private openCreateTransactionDialog(): void {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||
data: {
|
||||
currency: null,
|
||||
date: new Date(),
|
||||
fee: 0,
|
||||
platformId: null,
|
||||
quantity: null,
|
||||
symbol: null,
|
||||
type: 'BUY',
|
||||
unitPrice: null
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((order: UpdateOrderDto) => {
|
||||
if (order) {
|
||||
this.dataService.postOrder(order).subscribe({
|
||||
next: () => {
|
||||
this.fetchOrders();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
|
||||
public openUpdateTransactionDialog({
|
||||
accountId,
|
||||
currency,
|
||||
dataSource,
|
||||
date,
|
||||
fee,
|
||||
id,
|
||||
@ -164,23 +137,30 @@ export class TransactionsPageComponent implements OnInit {
|
||||
}: OrderModel): void {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||
data: {
|
||||
currency,
|
||||
date,
|
||||
fee,
|
||||
id,
|
||||
platformId,
|
||||
quantity,
|
||||
symbol,
|
||||
type,
|
||||
unitPrice
|
||||
accounts: this.user.accounts,
|
||||
transaction: {
|
||||
accountId,
|
||||
currency,
|
||||
dataSource,
|
||||
date,
|
||||
fee,
|
||||
id,
|
||||
platformId,
|
||||
quantity,
|
||||
symbol,
|
||||
type,
|
||||
unitPrice
|
||||
}
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((order: UpdateOrderDto) => {
|
||||
if (order) {
|
||||
this.dataService.putOrder(order).subscribe({
|
||||
dialogRef.afterClosed().subscribe((data: any) => {
|
||||
const transaction: UpdateOrderDto = data?.transaction;
|
||||
|
||||
if (transaction) {
|
||||
this.dataService.putOrder(transaction).subscribe({
|
||||
next: () => {
|
||||
this.fetchOrders();
|
||||
}
|
||||
@ -195,4 +175,41 @@ export class TransactionsPageComponent implements OnInit {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openCreateTransactionDialog(): void {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||
data: {
|
||||
accounts: this.user?.accounts,
|
||||
transaction: {
|
||||
accountId: this.user?.accounts.find((account) => {
|
||||
return account.isDefault;
|
||||
})?.id,
|
||||
currency: null,
|
||||
date: new Date(),
|
||||
fee: 0,
|
||||
platformId: null,
|
||||
quantity: null,
|
||||
symbol: null,
|
||||
type: 'BUY',
|
||||
unitPrice: null
|
||||
}
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((data: any) => {
|
||||
const transaction: UpdateOrderDto = data?.transaction;
|
||||
|
||||
if (transaction) {
|
||||
this.dataService.postOrder(transaction).subscribe({
|
||||
next: () => {
|
||||
this.fetchOrders();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"background_color": "transparent",
|
||||
"categories": ["finance", "utilities"],
|
||||
"description": "Privacy-first Portfolio Tracker",
|
||||
"description": "Open Source Portfolio Tracker",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Ghostfolio – Privacy-first Portfolio Tracker</title>
|
||||
<title>Ghostfolio – Open Source Portfolio Tracker</title>
|
||||
<base href="/" />
|
||||
<meta charset="utf-8" />
|
||||
<meta content="yes" name="apple-mobile-web-app-capable" />
|
||||
@ -25,13 +25,13 @@
|
||||
/>
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="Ghostfolio – Privacy-first Portfolio Tracker"
|
||||
content="Ghostfolio – Open Source Portfolio Tracker"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:description" content="" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Ghostfolio – Privacy-first Portfolio Tracker"
|
||||
content="Ghostfolio – Open Source Portfolio Tracker"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://www.ghostfol.io" />
|
||||
@ -42,7 +42,7 @@
|
||||
<meta property="og:updated_time" content="2021-03-20T00:00:00+00:00" />
|
||||
<meta
|
||||
property="og:site_name"
|
||||
content="Ghostfolio – Privacy-first Portfolio Tracker"
|
||||
content="Ghostfolio – Open Source Portfolio Tracker"
|
||||
/>
|
||||
|
||||
<link
|
||||
|
@ -57,11 +57,20 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.mat-card:not([class*='mat-elevation-z']) {
|
||||
border-color: rgba(
|
||||
var(--light-primary-text),
|
||||
var(--palette-foreground-divider-alpha)
|
||||
);
|
||||
.mat-card {
|
||||
background: var(--dark-background);
|
||||
|
||||
&:not([class*='mat-elevation-z']) {
|
||||
border-color: rgba(
|
||||
var(--light-primary-text),
|
||||
var(--palette-foreground-divider-alpha)
|
||||
);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-dialog-container {
|
||||
background: var(--dark-background);
|
||||
}
|
||||
|
||||
.mat-fab,
|
||||
|
@ -12,5 +12,9 @@
|
||||
{
|
||||
"path": "./tsconfig.editor.json"
|
||||
}
|
||||
]
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"strictInjectionParameters": true,
|
||||
"strictTemplates": false
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "0.91.0",
|
||||
"version": "0.96.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
@ -59,22 +59,23 @@ model MarketData {
|
||||
}
|
||||
|
||||
model Order {
|
||||
Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId])
|
||||
Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId])
|
||||
accountId String?
|
||||
accountUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
currency Currency
|
||||
dataSource DataSource @default(YAHOO)
|
||||
date DateTime
|
||||
fee Float
|
||||
id String @default(uuid())
|
||||
Platform Platform? @relation(fields: [platformId], references: [id])
|
||||
id String @default(uuid())
|
||||
Platform Platform? @relation(fields: [platformId], references: [id])
|
||||
platformId String?
|
||||
quantity Float
|
||||
symbol String
|
||||
type Type
|
||||
unitPrice Float
|
||||
updatedAt DateTime @updatedAt
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
updatedAt DateTime @updatedAt
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
@@id([id, userId])
|
||||
@ -128,6 +129,12 @@ enum Currency {
|
||||
USD
|
||||
}
|
||||
|
||||
enum DataSource {
|
||||
GHOSTFOLIO
|
||||
RAKUTEN
|
||||
YAHOO
|
||||
}
|
||||
|
||||
enum Provider {
|
||||
ANONYMOUS
|
||||
GOOGLE
|
||||
|
@ -82,6 +82,16 @@ async function main() {
|
||||
create: {
|
||||
accessToken:
|
||||
'c689bcc894e4a420cb609ee34271f3e07f200594f7d199c50d75add7102889eb60061a04cd2792ebc853c54e37308271271e7bf588657c9e0c37faacbc28c3c6',
|
||||
Account: {
|
||||
create: [
|
||||
{
|
||||
accountType: AccountType.SECURITIES,
|
||||
id: 'f4425b66-9ba9-4ac4-93d7-fdf9a145e8cb',
|
||||
isDefault: true,
|
||||
name: 'Default Account'
|
||||
}
|
||||
]
|
||||
},
|
||||
alias: 'Admin',
|
||||
id: '4e1af723-95f6-44f8-92a7-464df17f6ec3',
|
||||
role: Role.ADMIN
|
||||
|
Reference in New Issue
Block a user