Merge branch 'main' of github.com:ghostfolio/ghostfolio
All checks were successful
Docker image CD / build_and_push (push) Successful in 12m42s
All checks were successful
Docker image CD / build_and_push (push) Successful in 12m42s
This commit is contained in:
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Introduced fuzzy search for the holdings of the assistant
|
||||||
- Introduced fuzzy search for the quick links of the assistant
|
- Introduced fuzzy search for the quick links of the assistant
|
||||||
- Improved the search results of the assistant to only display categories with content
|
- Improved the search results of the assistant to only display categories with content
|
||||||
- Enhanced the sitemap to dynamically compose public routes
|
- Enhanced the sitemap to dynamically compose public routes
|
||||||
|
@ -412,19 +412,19 @@ export class PortfolioController {
|
|||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
filterByDataSource,
|
filterByDataSource,
|
||||||
filterByHoldingType,
|
filterByHoldingType,
|
||||||
filterBySearchQuery,
|
|
||||||
filterBySymbol,
|
filterBySymbol,
|
||||||
filterByTags
|
filterByTags
|
||||||
});
|
});
|
||||||
|
|
||||||
const { holdings } = await this.portfolioService.getDetails({
|
const holdings = await this.portfolioService.getHoldings({
|
||||||
dateRange,
|
dateRange,
|
||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
|
query: filterBySearchQuery,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
return { holdings: Object.values(holdings) };
|
return { holdings };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('investments')
|
@Get('investments')
|
||||||
|
@ -49,7 +49,6 @@ import {
|
|||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioReportResponse,
|
PortfolioReportResponse,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
|
||||||
UserSettings
|
UserSettings
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/models';
|
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||||
@ -92,6 +91,8 @@ import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
|||||||
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
|
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
|
||||||
import { RulesService } from './rules.service';
|
import { RulesService } from './rules.service';
|
||||||
|
|
||||||
|
const Fuse = require('fuse.js');
|
||||||
|
|
||||||
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
||||||
const developedMarkets = require('../../assets/countries/developed-markets.json');
|
const developedMarkets = require('../../assets/countries/developed-markets.json');
|
||||||
const emergingMarkets = require('../../assets/countries/emerging-markets.json');
|
const emergingMarkets = require('../../assets/countries/emerging-markets.json');
|
||||||
@ -269,6 +270,43 @@ export class PortfolioService {
|
|||||||
return dividends;
|
return dividends;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getHoldings({
|
||||||
|
dateRange,
|
||||||
|
filters,
|
||||||
|
impersonationId,
|
||||||
|
query,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
dateRange: DateRange;
|
||||||
|
filters?: Filter[];
|
||||||
|
impersonationId: string;
|
||||||
|
query?: string;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
|
const { holdings: holdingsMap } = await this.getDetails({
|
||||||
|
dateRange,
|
||||||
|
filters,
|
||||||
|
impersonationId,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
let holdings = Object.values(holdingsMap);
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const fuse = new Fuse(holdings, {
|
||||||
|
keys: ['isin', 'name', 'symbol'],
|
||||||
|
threshold: 0.3
|
||||||
|
});
|
||||||
|
|
||||||
|
holdings = fuse.search(query).map(({ item }) => {
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return holdings;
|
||||||
|
}
|
||||||
|
|
||||||
public async getInvestments({
|
public async getInvestments({
|
||||||
dateRange,
|
dateRange,
|
||||||
filters,
|
filters,
|
||||||
@ -977,155 +1015,6 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHoldings({
|
|
||||||
dateRange = 'max',
|
|
||||||
filters,
|
|
||||||
impersonationId
|
|
||||||
}: {
|
|
||||||
dateRange?: DateRange;
|
|
||||||
filters?: Filter[];
|
|
||||||
impersonationId: string;
|
|
||||||
}): Promise<{ hasErrors: boolean; positions: Position[] }> {
|
|
||||||
const searchQuery = filters.find(({ type }) => {
|
|
||||||
return type === 'SEARCH_QUERY';
|
|
||||||
})?.id;
|
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
|
||||||
const user = await this.userService.user({ id: userId });
|
|
||||||
const userCurrency = this.getUserCurrency(user);
|
|
||||||
|
|
||||||
const { activities } =
|
|
||||||
await this.orderService.getOrdersForPortfolioCalculator({
|
|
||||||
filters,
|
|
||||||
userCurrency,
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (activities.length === 0) {
|
|
||||||
return {
|
|
||||||
hasErrors: false,
|
|
||||||
positions: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
|
||||||
activities,
|
|
||||||
filters,
|
|
||||||
userId,
|
|
||||||
calculationType: this.getUserPerformanceCalculationType(user),
|
|
||||||
currency: userCurrency
|
|
||||||
});
|
|
||||||
|
|
||||||
const portfolioSnapshot = await portfolioCalculator.getSnapshot();
|
|
||||||
const hasErrors = portfolioSnapshot.hasErrors;
|
|
||||||
let positions = portfolioSnapshot.positions;
|
|
||||||
|
|
||||||
positions = positions.filter(({ quantity }) => {
|
|
||||||
return !quantity.eq(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => {
|
|
||||||
return {
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
|
||||||
this.dataProviderService.getQuotes({
|
|
||||||
user,
|
|
||||||
items: assetProfileIdentifiers
|
|
||||||
}),
|
|
||||||
this.symbolProfileService.getSymbolProfiles(
|
|
||||||
positions.map(({ dataSource, symbol }) => {
|
|
||||||
return { dataSource, symbol };
|
|
||||||
})
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
|
||||||
|
|
||||||
for (const symbolProfile of symbolProfiles) {
|
|
||||||
symbolProfileMap[symbolProfile.symbol] = symbolProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchQuery) {
|
|
||||||
positions = positions.filter(({ symbol }) => {
|
|
||||||
const enhancedSymbolProfile = symbolProfileMap[symbol];
|
|
||||||
|
|
||||||
return (
|
|
||||||
enhancedSymbolProfile.isin?.toLowerCase().startsWith(searchQuery) ||
|
|
||||||
enhancedSymbolProfile.name?.toLowerCase().startsWith(searchQuery) ||
|
|
||||||
enhancedSymbolProfile.symbol?.toLowerCase().startsWith(searchQuery)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasErrors,
|
|
||||||
positions: positions.map(
|
|
||||||
({
|
|
||||||
averagePrice,
|
|
||||||
currency,
|
|
||||||
dataSource,
|
|
||||||
firstBuyDate,
|
|
||||||
grossPerformance,
|
|
||||||
grossPerformancePercentage,
|
|
||||||
grossPerformancePercentageWithCurrencyEffect,
|
|
||||||
grossPerformanceWithCurrencyEffect,
|
|
||||||
investment,
|
|
||||||
investmentWithCurrencyEffect,
|
|
||||||
netPerformance,
|
|
||||||
netPerformancePercentage,
|
|
||||||
netPerformancePercentageWithCurrencyEffectMap,
|
|
||||||
netPerformanceWithCurrencyEffectMap,
|
|
||||||
quantity,
|
|
||||||
symbol,
|
|
||||||
timeWeightedInvestment,
|
|
||||||
timeWeightedInvestmentWithCurrencyEffect,
|
|
||||||
transactionCount
|
|
||||||
}) => {
|
|
||||||
return {
|
|
||||||
currency,
|
|
||||||
dataSource,
|
|
||||||
firstBuyDate,
|
|
||||||
symbol,
|
|
||||||
transactionCount,
|
|
||||||
assetClass: symbolProfileMap[symbol].assetClass,
|
|
||||||
assetSubClass: symbolProfileMap[symbol].assetSubClass,
|
|
||||||
averagePrice: averagePrice.toNumber(),
|
|
||||||
grossPerformance: grossPerformance?.toNumber() ?? null,
|
|
||||||
grossPerformancePercentage:
|
|
||||||
grossPerformancePercentage?.toNumber() ?? null,
|
|
||||||
grossPerformancePercentageWithCurrencyEffect:
|
|
||||||
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? null,
|
|
||||||
grossPerformanceWithCurrencyEffect:
|
|
||||||
grossPerformanceWithCurrencyEffect?.toNumber() ?? null,
|
|
||||||
investment: investment.toNumber(),
|
|
||||||
investmentWithCurrencyEffect:
|
|
||||||
investmentWithCurrencyEffect?.toNumber(),
|
|
||||||
marketState:
|
|
||||||
dataProviderResponses[symbol]?.marketState ?? 'delayed',
|
|
||||||
name: symbolProfileMap[symbol].name,
|
|
||||||
netPerformance: netPerformance?.toNumber() ?? null,
|
|
||||||
netPerformancePercentage:
|
|
||||||
netPerformancePercentage?.toNumber() ?? null,
|
|
||||||
netPerformancePercentageWithCurrencyEffect:
|
|
||||||
netPerformancePercentageWithCurrencyEffectMap?.[
|
|
||||||
dateRange
|
|
||||||
]?.toNumber() ?? null,
|
|
||||||
netPerformanceWithCurrencyEffect:
|
|
||||||
netPerformanceWithCurrencyEffectMap?.[dateRange]?.toNumber() ??
|
|
||||||
null,
|
|
||||||
quantity: quantity.toNumber(),
|
|
||||||
timeWeightedInvestment: timeWeightedInvestment?.toNumber(),
|
|
||||||
timeWeightedInvestmentWithCurrencyEffect:
|
|
||||||
timeWeightedInvestmentWithCurrencyEffect?.toNumber()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getPerformance({
|
public async getPerformance({
|
||||||
dateRange = 'max',
|
dateRange = 'max',
|
||||||
filters,
|
filters,
|
||||||
|
Reference in New Issue
Block a user