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
|
||||
|
||||
- Introduced fuzzy search for the holdings 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
|
||||
- Enhanced the sitemap to dynamically compose public routes
|
||||
|
@ -412,19 +412,19 @@ export class PortfolioController {
|
||||
filterByAssetClasses,
|
||||
filterByDataSource,
|
||||
filterByHoldingType,
|
||||
filterBySearchQuery,
|
||||
filterBySymbol,
|
||||
filterByTags
|
||||
});
|
||||
|
||||
const { holdings } = await this.portfolioService.getDetails({
|
||||
const holdings = await this.portfolioService.getHoldings({
|
||||
dateRange,
|
||||
filters,
|
||||
impersonationId,
|
||||
query: filterBySearchQuery,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
return { holdings: Object.values(holdings) };
|
||||
return { holdings };
|
||||
}
|
||||
|
||||
@Get('investments')
|
||||
|
@ -49,7 +49,6 @@ import {
|
||||
PortfolioPosition,
|
||||
PortfolioReportResponse,
|
||||
PortfolioSummary,
|
||||
Position,
|
||||
UserSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||
@ -92,6 +91,8 @@ import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
||||
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
|
||||
import { RulesService } from './rules.service';
|
||||
|
||||
const Fuse = require('fuse.js');
|
||||
|
||||
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
||||
const developedMarkets = require('../../assets/countries/developed-markets.json');
|
||||
const emergingMarkets = require('../../assets/countries/emerging-markets.json');
|
||||
@ -269,6 +270,43 @@ export class PortfolioService {
|
||||
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({
|
||||
dateRange,
|
||||
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({
|
||||
dateRange = 'max',
|
||||
filters,
|
||||
|
Reference in New Issue
Block a user