mirror of https://github.com/ghostfolio/ghostfolio
116 changed files with 4697 additions and 4285 deletions
@ -0,0 +1,10 @@ |
|||||
|
import { DataSource } from '@prisma/client'; |
||||
|
import { IsEnum, IsString } from 'class-validator'; |
||||
|
|
||||
|
export class CreateWatchlistItemDto { |
||||
|
@IsEnum(DataSource) |
||||
|
dataSource: DataSource; |
||||
|
|
||||
|
@IsString() |
||||
|
symbol: string; |
||||
|
} |
@ -0,0 +1,85 @@ |
|||||
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; |
||||
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; |
||||
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; |
||||
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; |
||||
|
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; |
||||
|
import { permissions } from '@ghostfolio/common/permissions'; |
||||
|
import { RequestWithUser } from '@ghostfolio/common/types'; |
||||
|
|
||||
|
import { |
||||
|
Body, |
||||
|
Controller, |
||||
|
Delete, |
||||
|
Get, |
||||
|
HttpException, |
||||
|
Inject, |
||||
|
Param, |
||||
|
Post, |
||||
|
UseGuards, |
||||
|
UseInterceptors |
||||
|
} from '@nestjs/common'; |
||||
|
import { REQUEST } from '@nestjs/core'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
import { DataSource } from '@prisma/client'; |
||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
||||
|
|
||||
|
import { CreateWatchlistItemDto } from './create-watchlist-item.dto'; |
||||
|
import { WatchlistService } from './watchlist.service'; |
||||
|
|
||||
|
@Controller('watchlist') |
||||
|
export class WatchlistController { |
||||
|
public constructor( |
||||
|
@Inject(REQUEST) private readonly request: RequestWithUser, |
||||
|
private readonly watchlistService: WatchlistService |
||||
|
) {} |
||||
|
|
||||
|
@Post() |
||||
|
@HasPermission(permissions.createWatchlistItem) |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor) |
||||
|
public async createWatchlistItem(@Body() data: CreateWatchlistItemDto) { |
||||
|
return this.watchlistService.createWatchlistItem({ |
||||
|
dataSource: data.dataSource, |
||||
|
symbol: data.symbol, |
||||
|
userId: this.request.user.id |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Delete(':dataSource/:symbol') |
||||
|
@HasPermission(permissions.deleteWatchlistItem) |
||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) |
||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor) |
||||
|
public async deleteWatchlistItem( |
||||
|
@Param('dataSource') dataSource: DataSource, |
||||
|
@Param('symbol') symbol: string |
||||
|
) { |
||||
|
const watchlistItem = await this.watchlistService |
||||
|
.getWatchlistItems(this.request.user.id) |
||||
|
.then((items) => { |
||||
|
return items.find((item) => { |
||||
|
return item.dataSource === dataSource && item.symbol === symbol; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
if (!watchlistItem) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.NOT_FOUND), |
||||
|
StatusCodes.NOT_FOUND |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return this.watchlistService.deleteWatchlistItem({ |
||||
|
dataSource, |
||||
|
symbol, |
||||
|
userId: this.request.user.id |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Get() |
||||
|
@HasPermission(permissions.readWatchlist) |
||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) |
||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor) |
||||
|
public async getWatchlistItems(): Promise<AssetProfileIdentifier[]> { |
||||
|
return this.watchlistService.getWatchlistItems(this.request.user.id); |
||||
|
} |
||||
|
} |
@ -0,0 +1,19 @@ |
|||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; |
||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; |
||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
||||
|
|
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { WatchlistController } from './watchlist.controller'; |
||||
|
import { WatchlistService } from './watchlist.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [WatchlistController], |
||||
|
imports: [ |
||||
|
PrismaModule, |
||||
|
TransformDataSourceInRequestModule, |
||||
|
TransformDataSourceInResponseModule |
||||
|
], |
||||
|
providers: [WatchlistService] |
||||
|
}) |
||||
|
export class WatchlistModule {} |
@ -0,0 +1,79 @@ |
|||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
||||
|
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
import { Injectable, NotFoundException } from '@nestjs/common'; |
||||
|
import { DataSource } from '@prisma/client'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class WatchlistService { |
||||
|
public constructor(private readonly prismaService: PrismaService) {} |
||||
|
|
||||
|
public async createWatchlistItem({ |
||||
|
dataSource, |
||||
|
symbol, |
||||
|
userId |
||||
|
}: { |
||||
|
dataSource: DataSource; |
||||
|
symbol: string; |
||||
|
userId: string; |
||||
|
}): Promise<void> { |
||||
|
const symbolProfile = await this.prismaService.symbolProfile.findUnique({ |
||||
|
where: { |
||||
|
dataSource_symbol: { dataSource, symbol } |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (!symbolProfile) { |
||||
|
throw new NotFoundException( |
||||
|
`Asset profile not found for ${symbol} (${dataSource})` |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
await this.prismaService.user.update({ |
||||
|
data: { |
||||
|
watchlist: { |
||||
|
connect: { |
||||
|
dataSource_symbol: { dataSource, symbol } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
where: { id: userId } |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async deleteWatchlistItem({ |
||||
|
dataSource, |
||||
|
symbol, |
||||
|
userId |
||||
|
}: { |
||||
|
dataSource: DataSource; |
||||
|
symbol: string; |
||||
|
userId: string; |
||||
|
}) { |
||||
|
await this.prismaService.user.update({ |
||||
|
data: { |
||||
|
watchlist: { |
||||
|
disconnect: { |
||||
|
dataSource_symbol: { dataSource, symbol } |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
where: { id: userId } |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async getWatchlistItems( |
||||
|
userId: string |
||||
|
): Promise<AssetProfileIdentifier[]> { |
||||
|
const user = await this.prismaService.user.findUnique({ |
||||
|
select: { |
||||
|
watchlist: { |
||||
|
select: { dataSource: true, symbol: true } |
||||
|
} |
||||
|
}, |
||||
|
where: { id: userId } |
||||
|
}); |
||||
|
|
||||
|
return user.watchlist ?? []; |
||||
|
} |
||||
|
} |
@ -0,0 +1,29 @@ |
|||||
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
||||
|
import { |
||||
|
AssetProfileIdentifier, |
||||
|
SymbolMetrics |
||||
|
} from '@ghostfolio/common/interfaces'; |
||||
|
import { PortfolioSnapshot } from '@ghostfolio/common/models'; |
||||
|
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
export class RoiPortfolioCalculator extends PortfolioCalculator { |
||||
|
protected calculateOverallPerformance(): PortfolioSnapshot { |
||||
|
throw new Error('Method not implemented.'); |
||||
|
} |
||||
|
|
||||
|
protected getPerformanceCalculationType() { |
||||
|
return PerformanceCalculationType.ROI; |
||||
|
} |
||||
|
|
||||
|
protected getSymbolMetrics({}: { |
||||
|
end: Date; |
||||
|
exchangeRates: { [dateString: string]: number }; |
||||
|
marketSymbolMap: { |
||||
|
[date: string]: { [symbol: string]: Big }; |
||||
|
}; |
||||
|
start: Date; |
||||
|
step?: number; |
||||
|
} & AssetProfileIdentifier): SymbolMetrics { |
||||
|
throw new Error('Method not implemented.'); |
||||
|
} |
||||
|
} |
File diff suppressed because it is too large
@ -0,0 +1,7 @@ |
|||||
|
export class AssetProfileDelistedError extends Error { |
||||
|
public constructor(message: string) { |
||||
|
super(message); |
||||
|
|
||||
|
this.name = 'AssetProfileDelistedError'; |
||||
|
} |
||||
|
} |
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,6 @@ |
|||||
|
export enum PerformanceCalculationType { |
||||
|
MWR = 'MWR', // Money-Weighted Rate of Return
|
||||
|
ROAI = 'ROAI', // Return on Average Investment
|
||||
|
ROI = 'ROI', // Return on Investment
|
||||
|
TWR = 'TWR' // Time-Weighted Rate of Return
|
||||
|
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue