mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
282 lines
7.3 KiB
282 lines
7.3 KiB
import { ImportService } from '@ghostfolio/api/app/import/import.service';
|
|
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
|
import {
|
|
transformDataSourceInRequest,
|
|
transformDataSourceInResponse
|
|
} from '@ghostfolio/api/helper/data-source.helper';
|
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
import { PortfolioTableService } from '@ghostfolio/api/services/portfolio-table/portfolio-table.service';
|
|
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
|
|
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|
|
|
import { HttpException, Injectable } from '@nestjs/common';
|
|
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
|
|
import { z } from 'zod';
|
|
|
|
import {
|
|
GET_ACCOUNTS_PARAMETERS,
|
|
GET_ACTIVITIES_PARAMETERS,
|
|
GET_PERFORMANCE_PARAMETERS,
|
|
IMPORT_ACTIVITIES_PARAMETERS,
|
|
SEARCH_ASSET_PROFILES_PARAMETERS
|
|
} from './mcp.schemas';
|
|
|
|
@Injectable()
|
|
export class McpService {
|
|
public constructor(
|
|
private readonly apiService: ApiService,
|
|
private readonly configurationService: ConfigurationService,
|
|
private readonly importService: ImportService,
|
|
private readonly portfolioTableService: PortfolioTableService,
|
|
private readonly symbolService: SymbolService,
|
|
private readonly userService: UserService
|
|
) {}
|
|
|
|
public async getAccounts({
|
|
accountIds,
|
|
assetClasses,
|
|
holding,
|
|
userId,
|
|
withValues
|
|
}: z.infer<typeof GET_ACCOUNTS_PARAMETERS> & {
|
|
userId: string;
|
|
withValues?: boolean;
|
|
}) {
|
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
|
...this.getHoldingFilterParameters({ holding }),
|
|
filterByAccounts: accountIds,
|
|
filterByAssetClasses: assetClasses
|
|
});
|
|
|
|
const table = await this.portfolioTableService.getAccountsTable({
|
|
filters,
|
|
userId,
|
|
withValues
|
|
});
|
|
|
|
return this.getTextResult(table);
|
|
}
|
|
|
|
public async getActivities({
|
|
activityTypes,
|
|
assetClasses,
|
|
holding,
|
|
range,
|
|
skip,
|
|
take,
|
|
userCurrency,
|
|
userId,
|
|
withValues
|
|
}: z.infer<typeof GET_ACTIVITIES_PARAMETERS> & {
|
|
userCurrency: string;
|
|
userId: string;
|
|
withValues?: boolean;
|
|
}) {
|
|
let endDate: Date | undefined;
|
|
let startDate: Date | undefined;
|
|
|
|
if (range) {
|
|
({ endDate, startDate } = getIntervalFromDateRange({
|
|
dateRange: range
|
|
}));
|
|
}
|
|
|
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
|
...this.getHoldingFilterParameters({ holding }),
|
|
filterByAssetClasses: assetClasses
|
|
});
|
|
|
|
const table = await this.portfolioTableService.getActivitiesTable({
|
|
endDate,
|
|
filters,
|
|
skip,
|
|
startDate,
|
|
take,
|
|
userCurrency,
|
|
userId,
|
|
withValues,
|
|
types: activityTypes
|
|
});
|
|
|
|
return this.getTextResult(table);
|
|
}
|
|
|
|
public async getPerformance({
|
|
accountIds,
|
|
assetClasses,
|
|
holding,
|
|
range,
|
|
userId,
|
|
withValues
|
|
}: z.infer<typeof GET_PERFORMANCE_PARAMETERS> & {
|
|
userId: string;
|
|
withValues?: boolean;
|
|
}) {
|
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
|
...this.getHoldingFilterParameters({ holding }),
|
|
filterByAccounts: accountIds,
|
|
filterByAssetClasses: assetClasses
|
|
});
|
|
|
|
const table = await this.portfolioTableService.getPerformanceTable({
|
|
filters,
|
|
userId,
|
|
withValues,
|
|
dateRange: range
|
|
});
|
|
|
|
return this.getTextResult(table);
|
|
}
|
|
|
|
public async getPortfolio({
|
|
userId,
|
|
withValues
|
|
}: {
|
|
userId: string;
|
|
withValues?: boolean;
|
|
}) {
|
|
const table = await this.portfolioTableService.getHoldingsTable({
|
|
userId,
|
|
withValues,
|
|
languageCode: DEFAULT_LANGUAGE_CODE,
|
|
withDataSource: true
|
|
});
|
|
|
|
return this.getTextResult(table);
|
|
}
|
|
|
|
public async getWatchlist({ userId }: { userId: string }) {
|
|
const table = await this.portfolioTableService.getWatchlistTable({
|
|
userId
|
|
});
|
|
|
|
return this.getTextResult(table);
|
|
}
|
|
|
|
public async importActivities({
|
|
activities,
|
|
userId
|
|
}: z.infer<typeof IMPORT_ACTIVITIES_PARAMETERS> & { userId: string }) {
|
|
const user = await this.getUserWithPermission({
|
|
userId,
|
|
permission: permissions.createActivity
|
|
});
|
|
|
|
const activitiesDto = activities.map((activity) => {
|
|
return {
|
|
...activity,
|
|
dataSource: transformDataSourceInRequest({
|
|
configurationService: this.configurationService,
|
|
dataSource: activity.dataSource
|
|
})
|
|
};
|
|
});
|
|
|
|
// The filter passes on the message of a CallerFacingError, which is
|
|
// written for the caller, and hides the message of every other error
|
|
const importedActivities = await this.importService.import({
|
|
activitiesDto,
|
|
user,
|
|
accountsWithBalancesDto: [],
|
|
assetProfilesWithMarketDataDto: [],
|
|
platformsDto: [],
|
|
tagsDto: []
|
|
});
|
|
|
|
const text = [
|
|
`Imported activities: ${importedActivities.length}`,
|
|
`Skipped duplicate activities: ${
|
|
activities.length - importedActivities.length
|
|
}`
|
|
].join('\n');
|
|
|
|
return this.getTextResult(text);
|
|
}
|
|
|
|
public async searchAssetProfiles({
|
|
query,
|
|
userId
|
|
}: z.infer<typeof SEARCH_ASSET_PROFILES_PARAMETERS> & { userId: string }) {
|
|
const user = await this.getUserWithPermission({
|
|
userId,
|
|
permission: permissions.createActivity
|
|
});
|
|
|
|
const { items } = await this.symbolService.lookup({ query, user });
|
|
|
|
const assetProfiles = items.flatMap(
|
|
({
|
|
assetClass,
|
|
assetSubClass,
|
|
currency,
|
|
dataProviderInfo,
|
|
dataSource,
|
|
name,
|
|
symbol
|
|
}) => {
|
|
if (!dataSource || dataProviderInfo.isPremium) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{
|
|
assetClass,
|
|
assetSubClass,
|
|
currency,
|
|
name,
|
|
symbol,
|
|
dataSource: transformDataSourceInResponse({
|
|
dataSource,
|
|
configurationService: this.configurationService
|
|
})
|
|
}
|
|
];
|
|
}
|
|
);
|
|
|
|
return this.getTextResult(JSON.stringify({ assetProfiles }, null, 2));
|
|
}
|
|
|
|
private getHoldingFilterParameters({
|
|
holding
|
|
}: Pick<z.infer<typeof GET_ACCOUNTS_PARAMETERS>, 'holding'>) {
|
|
return {
|
|
filterByDataSource: transformDataSourceInRequest({
|
|
configurationService: this.configurationService,
|
|
dataSource: holding?.dataSource
|
|
}),
|
|
filterBySymbol: holding?.symbol
|
|
};
|
|
}
|
|
|
|
private getTextResult(text: string) {
|
|
return { content: [{ text, type: 'text' as const }] };
|
|
}
|
|
|
|
/**
|
|
* Gives the user of the access, if the role of the user has the permission.
|
|
* The scope of the access is evaluated separately by the ScopeGuard, hence a
|
|
* tool which changes data has to call this.
|
|
*/
|
|
private async getUserWithPermission({
|
|
permission,
|
|
userId
|
|
}: {
|
|
permission: string;
|
|
userId: string;
|
|
}) {
|
|
const user = await this.userService.user({ id: userId });
|
|
|
|
if (!hasPermission(user?.permissions, permission)) {
|
|
throw new HttpException(
|
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
|
StatusCodes.FORBIDDEN
|
|
);
|
|
}
|
|
|
|
return user;
|
|
}
|
|
}
|
|
|