mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
96 changed files with 20492 additions and 1280 deletions
@ -0,0 +1 @@ |
|||||
|
14d4daf73eefed7da7c32ec19bc37e678be0244fb46c8f4965bfe9ece7384706ed58222ad9b96323893c1d845bc33a308e7524c2c79636062cbb095e0780cb51 |
@ -1,25 +0,0 @@ |
|||||
COMPOSE_PROJECT_NAME=ghostfolio-development |
|
||||
|
|
||||
# CACHE |
|
||||
REDIS_HOST=localhost |
|
||||
REDIS_PORT=6379 |
|
||||
REDIS_PASSWORD=<INSERT_REDIS_PASSWORD> |
|
||||
|
|
||||
# POSTGRES |
|
||||
POSTGRES_DB=ghostfolio-db |
|
||||
POSTGRES_USER=user |
|
||||
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD> |
|
||||
|
|
||||
# VARIOUS |
|
||||
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING> |
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer |
|
||||
JWT_SECRET_KEY=<INSERT_RANDOM_STRING> |
|
||||
|
|
||||
# DEVELOPMENT |
|
||||
|
|
||||
# Nx 18 enables using plugins to infer targets by default |
|
||||
# This is disabled for existing workspaces to maintain compatibility |
|
||||
# For more info, see: https://nx.dev/concepts/inferred-tasks |
|
||||
NX_ADD_PLUGINS=false |
|
||||
|
|
||||
NX_NATIVE_COMMAND_RUNNER=false |
|
@ -0,0 +1,47 @@ |
|||||
|
name: Docker image CD - Branch |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
branches: |
||||
|
- '*' |
||||
|
|
||||
|
jobs: |
||||
|
build_and_push: |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v3 |
||||
|
|
||||
|
- name: Docker metadata |
||||
|
id: meta |
||||
|
uses: docker/metadata-action@v4 |
||||
|
with: |
||||
|
images: dandevaud/ghostfolio |
||||
|
tags: | |
||||
|
type=semver,pattern={{major}} |
||||
|
type=semver,pattern={{version}} |
||||
|
|
||||
|
- name: Set up QEMU |
||||
|
uses: docker/setup-qemu-action@v2 |
||||
|
|
||||
|
- name: Set up Docker Buildx |
||||
|
id: buildx |
||||
|
uses: docker/setup-buildx-action@v2 |
||||
|
|
||||
|
- name: Login to DockerHub |
||||
|
if: github.event_name != 'pull_request' |
||||
|
uses: docker/login-action@v2 |
||||
|
with: |
||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }} |
||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} |
||||
|
|
||||
|
- name: Build and push |
||||
|
uses: docker/build-push-action@v3 |
||||
|
with: |
||||
|
context: . |
||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64 |
||||
|
push: ${{ github.event_name != 'pull_request' }} |
||||
|
tags: dandevaud/ghostfolio:${{ github.ref_name }} |
||||
|
labels: ${{ steps.meta.output.labels }} |
||||
|
cache-from: type=gha |
||||
|
cache-to: type=gha,mode=max |
@ -0,0 +1,47 @@ |
|||||
|
name: Docker image CD - DEV |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
branches: |
||||
|
- 'dockerpush' |
||||
|
|
||||
|
jobs: |
||||
|
build_and_push: |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v3 |
||||
|
|
||||
|
- name: Docker metadata |
||||
|
id: meta |
||||
|
uses: docker/metadata-action@v4 |
||||
|
with: |
||||
|
images: dandevaud/ghostfolio |
||||
|
tags: | |
||||
|
type=semver,pattern={{major}} |
||||
|
type=semver,pattern={{version}} |
||||
|
|
||||
|
- name: Set up QEMU |
||||
|
uses: docker/setup-qemu-action@v2 |
||||
|
|
||||
|
- name: Set up Docker Buildx |
||||
|
id: buildx |
||||
|
uses: docker/setup-buildx-action@v2 |
||||
|
|
||||
|
- name: Login to DockerHub |
||||
|
if: github.event_name != 'pull_request' |
||||
|
uses: docker/login-action@v2 |
||||
|
with: |
||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }} |
||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} |
||||
|
|
||||
|
- name: Build and push |
||||
|
uses: docker/build-push-action@v3 |
||||
|
with: |
||||
|
context: . |
||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64 |
||||
|
push: ${{ github.event_name != 'pull_request' }} |
||||
|
tags: dandevaud/ghostfolio:beta |
||||
|
labels: ${{ steps.meta.output.labels }} |
||||
|
cache-from: type=gha |
||||
|
cache-to: type=gha,mode=max |
@ -0,0 +1,47 @@ |
|||||
|
name: Docker image CD - DEV |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
branches: |
||||
|
- 'main' |
||||
|
|
||||
|
jobs: |
||||
|
build_and_push: |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v3 |
||||
|
|
||||
|
- name: Docker metadata |
||||
|
id: meta |
||||
|
uses: docker/metadata-action@v4 |
||||
|
with: |
||||
|
images: dandevaud/ghostfolio |
||||
|
tags: | |
||||
|
type=semver,pattern={{major}} |
||||
|
type=semver,pattern={{version}} |
||||
|
|
||||
|
- name: Set up QEMU |
||||
|
uses: docker/setup-qemu-action@v2 |
||||
|
|
||||
|
- name: Set up Docker Buildx |
||||
|
id: buildx |
||||
|
uses: docker/setup-buildx-action@v2 |
||||
|
|
||||
|
- name: Login to DockerHub |
||||
|
if: github.event_name != 'pull_request' |
||||
|
uses: docker/login-action@v2 |
||||
|
with: |
||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }} |
||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} |
||||
|
|
||||
|
- name: Build and push |
||||
|
uses: docker/build-push-action@v3 |
||||
|
with: |
||||
|
context: . |
||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64 |
||||
|
push: ${{ github.event_name != 'pull_request' }} |
||||
|
tags: dandevaud/ghostfolio:main |
||||
|
labels: ${{ steps.meta.output.labels }} |
||||
|
cache-from: type=gha |
||||
|
cache-to: type=gha,mode=max |
@ -0,0 +1,595 @@ |
|||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service'; |
||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
||||
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; |
||||
|
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; |
||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; |
||||
|
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
||||
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; |
||||
|
import { Filter, HistoricalDataItem } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
import { Inject, Logger } from '@nestjs/common'; |
||||
|
import { Big } from 'big.js'; |
||||
|
import { |
||||
|
addDays, |
||||
|
eachDayOfInterval, |
||||
|
endOfDay, |
||||
|
format, |
||||
|
isAfter, |
||||
|
isBefore, |
||||
|
subDays |
||||
|
} from 'date-fns'; |
||||
|
|
||||
|
import { CurrentRateService } from '../../current-rate.service'; |
||||
|
import { DateQuery } from '../../interfaces/date-query.interface'; |
||||
|
import { PortfolioOrder } from '../../interfaces/portfolio-order.interface'; |
||||
|
import { RoaiPortfolioCalculator } from '../roai/portfolio-calculator'; |
||||
|
|
||||
|
export class CPRPortfolioCalculator extends RoaiPortfolioCalculator { |
||||
|
private holdings: { [date: string]: { [symbol: string]: Big } } = {}; |
||||
|
private holdingCurrencies: { [symbol: string]: string } = {}; |
||||
|
|
||||
|
constructor( |
||||
|
{ |
||||
|
accountBalanceItems, |
||||
|
activities, |
||||
|
configurationService, |
||||
|
currency, |
||||
|
currentRateService, |
||||
|
exchangeRateDataService, |
||||
|
portfolioSnapshotService, |
||||
|
redisCacheService, |
||||
|
userId, |
||||
|
filters |
||||
|
}: { |
||||
|
accountBalanceItems: HistoricalDataItem[]; |
||||
|
activities: Activity[]; |
||||
|
configurationService: ConfigurationService; |
||||
|
currency: string; |
||||
|
currentRateService: CurrentRateService; |
||||
|
exchangeRateDataService: ExchangeRateDataService; |
||||
|
portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
redisCacheService: RedisCacheService; |
||||
|
filters: Filter[]; |
||||
|
userId: string; |
||||
|
}, |
||||
|
@Inject() |
||||
|
private orderService: OrderService |
||||
|
) { |
||||
|
super({ |
||||
|
accountBalanceItems, |
||||
|
activities, |
||||
|
configurationService, |
||||
|
currency, |
||||
|
filters, |
||||
|
currentRateService, |
||||
|
exchangeRateDataService, |
||||
|
portfolioSnapshotService, |
||||
|
redisCacheService, |
||||
|
userId |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
public async getPerformanceWithTimeWeightedReturn({ |
||||
|
start, |
||||
|
end |
||||
|
}: { |
||||
|
start: Date; |
||||
|
end: Date; |
||||
|
}): Promise<{ chart: HistoricalDataItem[] }> { |
||||
|
const item = await super.getPerformance({ |
||||
|
end, |
||||
|
start |
||||
|
}); |
||||
|
|
||||
|
const itemResult = item.chart; |
||||
|
const dates = itemResult.map((item) => parseDate(item.date)); |
||||
|
const timeWeighted = await this.getTimeWeightedChartData({ |
||||
|
dates |
||||
|
}); |
||||
|
|
||||
|
item.chart = itemResult.map((itemInt) => { |
||||
|
const timeWeightedItem = timeWeighted.find( |
||||
|
(timeWeightedItem) => timeWeightedItem.date === itemInt.date |
||||
|
); |
||||
|
if (timeWeightedItem) { |
||||
|
itemInt.timeWeightedPerformance = |
||||
|
timeWeightedItem.netPerformanceInPercentage; |
||||
|
itemInt.timeWeightedPerformanceWithCurrencyEffect = |
||||
|
timeWeightedItem.netPerformanceInPercentageWithCurrencyEffect; |
||||
|
} |
||||
|
|
||||
|
return itemInt; |
||||
|
}); |
||||
|
return item; |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
public async getUnfilteredNetWorth(currency: string): Promise<Big> { |
||||
|
const activities = await this.orderService.getOrders({ |
||||
|
userId: this.userId, |
||||
|
userCurrency: currency, |
||||
|
types: ['BUY', 'SELL', 'STAKE'], |
||||
|
withExcludedAccounts: true |
||||
|
}); |
||||
|
const orders = this.activitiesToPortfolioOrder(activities.activities); |
||||
|
const start = orders.reduce( |
||||
|
(date, order) => |
||||
|
parseDate(date.date).getTime() < parseDate(order.date).getTime() |
||||
|
? date |
||||
|
: order, |
||||
|
{ date: orders[0].date } |
||||
|
).date; |
||||
|
|
||||
|
const end = new Date(Date.now()); |
||||
|
|
||||
|
const holdings = await this.getHoldings(orders, parseDate(start), end); |
||||
|
const marketMap = await this.currentRateService.getValues({ |
||||
|
dataGatheringItems: this.mapToDataGatheringItems(orders), |
||||
|
dateQuery: { in: [end] } |
||||
|
}); |
||||
|
const endString = format(end, DATE_FORMAT); |
||||
|
const exchangeRates = await Promise.all( |
||||
|
Object.keys(holdings[endString]).map(async (holding) => { |
||||
|
const symbolCurrency = this.getCurrencyFromActivities(orders, holding); |
||||
|
const exchangeRate = |
||||
|
await this.exchangeRateDataService.toCurrencyAtDate( |
||||
|
1, |
||||
|
symbolCurrency, |
||||
|
this.currency, |
||||
|
end |
||||
|
); |
||||
|
return { symbolCurrency, exchangeRate }; |
||||
|
}) |
||||
|
); |
||||
|
const currencyRates = exchangeRates.reduce<{ [currency: string]: number }>( |
||||
|
(all, currency): { [currency: string]: number } => { |
||||
|
all[currency.symbolCurrency] ??= currency.exchangeRate; |
||||
|
return all; |
||||
|
}, |
||||
|
{} |
||||
|
); |
||||
|
|
||||
|
const totalInvestment = await Object.keys(holdings[endString]).reduce( |
||||
|
(sum, holding) => { |
||||
|
if (!holdings[endString][holding].toNumber()) { |
||||
|
return sum; |
||||
|
} |
||||
|
const symbol = marketMap.values.find((m) => m.symbol === holding); |
||||
|
|
||||
|
if (symbol?.marketPrice === undefined) { |
||||
|
Logger.warn( |
||||
|
`Missing historical market data for ${holding} (${end})`, |
||||
|
'PortfolioCalculator' |
||||
|
); |
||||
|
return sum; |
||||
|
} else { |
||||
|
const symbolCurrency = this.getCurrency(holding); |
||||
|
const price = new Big(currencyRates[symbolCurrency]).mul( |
||||
|
symbol.marketPrice |
||||
|
); |
||||
|
return sum.plus(new Big(price).mul(holdings[endString][holding])); |
||||
|
} |
||||
|
}, |
||||
|
new Big(0) |
||||
|
); |
||||
|
return totalInvestment; |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected async getTimeWeightedChartData({ |
||||
|
dates |
||||
|
}: { |
||||
|
dates?: Date[]; |
||||
|
}): Promise<HistoricalDataItem[]> { |
||||
|
dates = dates.sort((a, b) => a.getTime() - b.getTime()); |
||||
|
const start = dates[0]; |
||||
|
const end = dates[dates.length - 1]; |
||||
|
const marketMapTask = this.computeMarketMap({ |
||||
|
gte: start, |
||||
|
lt: addDays(end, 1) |
||||
|
}); |
||||
|
const timelineHoldings = await this.getHoldings( |
||||
|
this.activities, |
||||
|
start, |
||||
|
end |
||||
|
); |
||||
|
|
||||
|
const data: HistoricalDataItem[] = []; |
||||
|
const startString = format(start, DATE_FORMAT); |
||||
|
|
||||
|
data.push({ |
||||
|
date: startString, |
||||
|
netPerformanceInPercentage: 0, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: 0, |
||||
|
investmentValueWithCurrencyEffect: 0, |
||||
|
netPerformance: 0, |
||||
|
netPerformanceWithCurrencyEffect: 0, |
||||
|
netWorth: 0, |
||||
|
totalAccountBalance: 0, |
||||
|
totalInvestment: 0, |
||||
|
totalInvestmentValueWithCurrencyEffect: 0, |
||||
|
value: 0, |
||||
|
valueWithCurrencyEffect: 0 |
||||
|
}); |
||||
|
|
||||
|
this.marketMap = await marketMapTask; |
||||
|
|
||||
|
let totalInvestment = Object.keys(timelineHoldings[startString]).reduce( |
||||
|
(sum, holding) => { |
||||
|
return sum.plus( |
||||
|
timelineHoldings[startString][holding].mul( |
||||
|
this.marketMap[startString][holding] ?? new Big(0) |
||||
|
) |
||||
|
); |
||||
|
}, |
||||
|
new Big(0) |
||||
|
); |
||||
|
|
||||
|
let previousNetPerformanceInPercentage = new Big(0); |
||||
|
let previousNetPerformanceInPercentageWithCurrencyEffect = new Big(0); |
||||
|
|
||||
|
for (let i = 1; i < dates.length; i++) { |
||||
|
const date = format(dates[i], DATE_FORMAT); |
||||
|
const previousDate = format(dates[i - 1], DATE_FORMAT); |
||||
|
const holdings = timelineHoldings[previousDate]; |
||||
|
let newTotalInvestment = new Big(0); |
||||
|
let netPerformanceInPercentage = new Big(0); |
||||
|
let netPerformanceInPercentageWithCurrencyEffect = new Big(0); |
||||
|
|
||||
|
for (const holding of Object.keys(holdings)) { |
||||
|
({ |
||||
|
netPerformanceInPercentage, |
||||
|
netPerformanceInPercentageWithCurrencyEffect, |
||||
|
newTotalInvestment |
||||
|
} = await this.handleSingleHolding( |
||||
|
previousDate, |
||||
|
holding, |
||||
|
date, |
||||
|
totalInvestment, |
||||
|
timelineHoldings, |
||||
|
netPerformanceInPercentage, |
||||
|
netPerformanceInPercentageWithCurrencyEffect, |
||||
|
newTotalInvestment |
||||
|
)); |
||||
|
} |
||||
|
totalInvestment = newTotalInvestment; |
||||
|
|
||||
|
previousNetPerformanceInPercentage = previousNetPerformanceInPercentage |
||||
|
.plus(1) |
||||
|
.mul(netPerformanceInPercentage.plus(1)) |
||||
|
.minus(1); |
||||
|
previousNetPerformanceInPercentageWithCurrencyEffect = |
||||
|
previousNetPerformanceInPercentageWithCurrencyEffect |
||||
|
.plus(1) |
||||
|
.mul(netPerformanceInPercentageWithCurrencyEffect.plus(1)) |
||||
|
.minus(1); |
||||
|
|
||||
|
data.push({ |
||||
|
date, |
||||
|
netPerformanceInPercentage: previousNetPerformanceInPercentage |
||||
|
.mul(100) |
||||
|
.toNumber(), |
||||
|
netPerformanceInPercentageWithCurrencyEffect: |
||||
|
previousNetPerformanceInPercentageWithCurrencyEffect |
||||
|
.mul(100) |
||||
|
.toNumber() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected async handleSingleHolding( |
||||
|
previousDate: string, |
||||
|
holding: string, |
||||
|
date: string, |
||||
|
totalInvestment: Big, |
||||
|
timelineHoldings: { [date: string]: { [symbol: string]: Big } }, |
||||
|
netPerformanceInPercentage: Big, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: Big, |
||||
|
newTotalInvestment: Big |
||||
|
) { |
||||
|
const previousPrice = |
||||
|
Object.keys(this.marketMap).indexOf(previousDate) > 0 |
||||
|
? this.marketMap[previousDate][holding] |
||||
|
: undefined; |
||||
|
const priceDictionary = this.marketMap[date]; |
||||
|
let currentPrice = |
||||
|
priceDictionary !== undefined ? priceDictionary[holding] : previousPrice; |
||||
|
currentPrice ??= previousPrice; |
||||
|
const previousHolding = timelineHoldings[previousDate][holding]; |
||||
|
|
||||
|
const priceInBaseCurrency = currentPrice |
||||
|
? new Big( |
||||
|
await this.exchangeRateDataService.toCurrencyAtDate( |
||||
|
currentPrice?.toNumber() ?? 0, |
||||
|
this.getCurrency(holding), |
||||
|
this.currency, |
||||
|
parseDate(date) |
||||
|
) |
||||
|
) |
||||
|
: new Big(0); |
||||
|
|
||||
|
if (previousHolding.eq(0)) { |
||||
|
return { |
||||
|
netPerformanceInPercentage: netPerformanceInPercentage, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: |
||||
|
netPerformanceInPercentageWithCurrencyEffect, |
||||
|
newTotalInvestment: newTotalInvestment.plus( |
||||
|
timelineHoldings[date][holding].mul(priceInBaseCurrency) |
||||
|
) |
||||
|
}; |
||||
|
} |
||||
|
if (previousPrice === undefined || currentPrice === undefined) { |
||||
|
Logger.warn( |
||||
|
`Missing historical market data for ${holding} (${previousPrice === undefined ? previousDate : date}})`, |
||||
|
'PortfolioCalculator' |
||||
|
); |
||||
|
return { |
||||
|
netPerformanceInPercentage: netPerformanceInPercentage, |
||||
|
netPerformanceInPercentageWithCurrencyEffect: |
||||
|
netPerformanceInPercentageWithCurrencyEffect, |
||||
|
newTotalInvestment: newTotalInvestment.plus( |
||||
|
timelineHoldings[date][holding].mul(priceInBaseCurrency) |
||||
|
) |
||||
|
}; |
||||
|
} |
||||
|
const previousPriceInBaseCurrency = previousPrice |
||||
|
? new Big( |
||||
|
await this.exchangeRateDataService.toCurrencyAtDate( |
||||
|
previousPrice?.toNumber() ?? 0, |
||||
|
this.getCurrency(holding), |
||||
|
this.currency, |
||||
|
parseDate(previousDate) |
||||
|
) |
||||
|
) |
||||
|
: new Big(0); |
||||
|
const portfolioWeight = totalInvestment.toNumber() |
||||
|
? previousHolding.mul(previousPriceInBaseCurrency).div(totalInvestment) |
||||
|
: new Big(0); |
||||
|
|
||||
|
netPerformanceInPercentage = netPerformanceInPercentage.plus( |
||||
|
currentPrice.div(previousPrice).minus(1).mul(portfolioWeight) |
||||
|
); |
||||
|
|
||||
|
netPerformanceInPercentageWithCurrencyEffect = |
||||
|
netPerformanceInPercentageWithCurrencyEffect.plus( |
||||
|
priceInBaseCurrency |
||||
|
.div(previousPriceInBaseCurrency) |
||||
|
.minus(1) |
||||
|
.mul(portfolioWeight) |
||||
|
); |
||||
|
|
||||
|
newTotalInvestment = newTotalInvestment.plus( |
||||
|
timelineHoldings[date][holding].mul(priceInBaseCurrency) |
||||
|
); |
||||
|
return { |
||||
|
netPerformanceInPercentage, |
||||
|
netPerformanceInPercentageWithCurrencyEffect, |
||||
|
newTotalInvestment |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected getCurrency(symbol: string) { |
||||
|
return this.getCurrencyFromActivities(this.activities, symbol); |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected getCurrencyFromActivities( |
||||
|
activities: PortfolioOrder[], |
||||
|
symbol: string |
||||
|
) { |
||||
|
if (!this.holdingCurrencies[symbol]) { |
||||
|
this.holdingCurrencies[symbol] = activities.find( |
||||
|
(a) => a.SymbolProfile.symbol === symbol |
||||
|
).SymbolProfile.currency; |
||||
|
} |
||||
|
|
||||
|
return this.holdingCurrencies[symbol]; |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected async getHoldings( |
||||
|
activities: PortfolioOrder[], |
||||
|
start: Date, |
||||
|
end: Date |
||||
|
) { |
||||
|
if ( |
||||
|
this.holdings && |
||||
|
Object.keys(this.holdings).some((h) => |
||||
|
isAfter(parseDate(h), subDays(end, 1)) |
||||
|
) && |
||||
|
Object.keys(this.holdings).some((h) => |
||||
|
isBefore(parseDate(h), addDays(start, 1)) |
||||
|
) |
||||
|
) { |
||||
|
return this.holdings; |
||||
|
} |
||||
|
|
||||
|
this.computeHoldings(activities, start, end); |
||||
|
return this.holdings; |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected async computeHoldings( |
||||
|
activities: PortfolioOrder[], |
||||
|
start: Date, |
||||
|
end: Date |
||||
|
) { |
||||
|
const investmentByDate = this.getInvestmentByDate(activities); |
||||
|
this.calculateHoldings(investmentByDate, start, end); |
||||
|
} |
||||
|
|
||||
|
private calculateHoldings( |
||||
|
investmentByDate: { [date: string]: PortfolioOrder[] }, |
||||
|
start: Date, |
||||
|
end: Date |
||||
|
) { |
||||
|
const transactionDates = Object.keys(investmentByDate).sort(); |
||||
|
const dates = eachDayOfInterval({ start, end }, { step: 1 }) |
||||
|
.map((date) => { |
||||
|
return resetHours(date); |
||||
|
}) |
||||
|
.sort((a, b) => a.getTime() - b.getTime()); |
||||
|
const currentHoldings: { [date: string]: { [symbol: string]: Big } } = {}; |
||||
|
|
||||
|
this.calculateInitialHoldings(investmentByDate, start, currentHoldings); |
||||
|
|
||||
|
for (let i = 1; i < dates.length; i++) { |
||||
|
const dateString = format(dates[i], DATE_FORMAT); |
||||
|
const previousDateString = format(dates[i - 1], DATE_FORMAT); |
||||
|
if (transactionDates.some((d) => d === dateString)) { |
||||
|
const holdings = { ...currentHoldings[previousDateString] }; |
||||
|
investmentByDate[dateString].forEach((trade) => { |
||||
|
holdings[trade.SymbolProfile.symbol] ??= new Big(0); |
||||
|
holdings[trade.SymbolProfile.symbol] = holdings[ |
||||
|
trade.SymbolProfile.symbol |
||||
|
].plus(trade.quantity.mul(getFactor(trade.type))); |
||||
|
}); |
||||
|
currentHoldings[dateString] = holdings; |
||||
|
} else { |
||||
|
currentHoldings[dateString] = currentHoldings[previousDateString]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.holdings = currentHoldings; |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected calculateInitialHoldings( |
||||
|
investmentByDate: { [date: string]: PortfolioOrder[] }, |
||||
|
start: Date, |
||||
|
currentHoldings: { [date: string]: { [symbol: string]: Big } } |
||||
|
) { |
||||
|
const preRangeTrades = Object.keys(investmentByDate) |
||||
|
.filter((date) => resetHours(new Date(date)) <= start) |
||||
|
.map((date) => investmentByDate[date]) |
||||
|
.reduce((a, b) => a.concat(b), []) |
||||
|
.reduce((groupBySymbol, trade) => { |
||||
|
if (!groupBySymbol[trade.SymbolProfile.symbol]) { |
||||
|
groupBySymbol[trade.SymbolProfile.symbol] = []; |
||||
|
} |
||||
|
|
||||
|
groupBySymbol[trade.SymbolProfile.symbol].push(trade); |
||||
|
|
||||
|
return groupBySymbol; |
||||
|
}, {}); |
||||
|
|
||||
|
currentHoldings[format(start, DATE_FORMAT)] = {}; |
||||
|
|
||||
|
for (const symbol of Object.keys(preRangeTrades)) { |
||||
|
const trades: PortfolioOrder[] = preRangeTrades[symbol]; |
||||
|
const startQuantity = trades.reduce((sum, trade) => { |
||||
|
return sum.plus(trade.quantity.mul(getFactor(trade.type))); |
||||
|
}, new Big(0)); |
||||
|
currentHoldings[format(start, DATE_FORMAT)][symbol] = startQuantity; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected getInvestmentByDate(activities: PortfolioOrder[]): { |
||||
|
[date: string]: PortfolioOrder[]; |
||||
|
} { |
||||
|
return activities.reduce((groupedByDate, order) => { |
||||
|
if (!groupedByDate[order.date]) { |
||||
|
groupedByDate[order.date] = []; |
||||
|
} |
||||
|
|
||||
|
groupedByDate[order.date].push(order); |
||||
|
|
||||
|
return groupedByDate; |
||||
|
}, {}); |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected mapToDataGatheringItems( |
||||
|
orders: PortfolioOrder[] |
||||
|
): IDataGatheringItem[] { |
||||
|
return orders |
||||
|
.map((activity) => { |
||||
|
return { |
||||
|
symbol: activity.SymbolProfile.symbol, |
||||
|
dataSource: activity.SymbolProfile.dataSource |
||||
|
}; |
||||
|
}) |
||||
|
.filter( |
||||
|
(gathering, i, arr) => |
||||
|
arr.findIndex((t) => t.symbol === gathering.symbol) === i |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected async computeMarketMap(dateQuery: DateQuery): Promise<{ |
||||
|
[date: string]: { [symbol: string]: Big }; |
||||
|
}> { |
||||
|
const dataGatheringItems: IDataGatheringItem[] = |
||||
|
this.mapToDataGatheringItems(this.activities); |
||||
|
const { values: marketSymbols } = await this.currentRateService.getValues({ |
||||
|
dataGatheringItems, |
||||
|
dateQuery |
||||
|
}); |
||||
|
|
||||
|
const marketSymbolMap: { |
||||
|
[date: string]: { [symbol: string]: Big }; |
||||
|
} = {}; |
||||
|
|
||||
|
for (const marketSymbol of marketSymbols) { |
||||
|
const date = format(marketSymbol.date, DATE_FORMAT); |
||||
|
|
||||
|
if (!marketSymbolMap[date]) { |
||||
|
marketSymbolMap[date] = {}; |
||||
|
} |
||||
|
|
||||
|
if (marketSymbol.marketPrice) { |
||||
|
marketSymbolMap[date][marketSymbol.symbol] = new Big( |
||||
|
marketSymbol.marketPrice |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return marketSymbolMap; |
||||
|
} |
||||
|
|
||||
|
@LogPerformance |
||||
|
protected activitiesToPortfolioOrder( |
||||
|
activities: Activity[] |
||||
|
): PortfolioOrder[] { |
||||
|
return activities |
||||
|
.map( |
||||
|
({ |
||||
|
date, |
||||
|
fee, |
||||
|
quantity, |
||||
|
SymbolProfile, |
||||
|
tags = [], |
||||
|
type, |
||||
|
unitPrice |
||||
|
}) => { |
||||
|
if (isAfter(date, new Date(Date.now()))) { |
||||
|
// Adapt date to today if activity is in future (e.g. liability)
|
||||
|
// to include it in the interval
|
||||
|
date = endOfDay(new Date(Date.now())); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
SymbolProfile, |
||||
|
tags, |
||||
|
type, |
||||
|
date: format(date, DATE_FORMAT), |
||||
|
fee: new Big(fee), |
||||
|
quantity: new Big(quantity), |
||||
|
unitPrice: new Big(unitPrice) |
||||
|
}; |
||||
|
} |
||||
|
) |
||||
|
.sort((a, b) => { |
||||
|
return a.date?.localeCompare(b.date); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -1,252 +1,252 @@ |
|||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; |
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; |
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
||||
import { |
import { |
||||
activityDummyData, |
activityDummyData, |
||||
loadActivityExportFile, |
loadActivityExportFile, |
||||
symbolProfileDummyData, |
symbolProfileDummyData, |
||||
userDummyData |
userDummyData |
||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
||||
import { |
import { |
||||
PerformanceCalculationType, |
PerformanceCalculationType, |
||||
PortfolioCalculatorFactory |
PortfolioCalculatorFactory |
||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
||||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
||||
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
||||
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
||||
import { parseDate } from '@ghostfolio/common/helper'; |
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
|
||||
import { Big } from 'big.js'; |
import { Big } from 'big.js'; |
||||
import { join } from 'path'; |
import { join } from 'path'; |
||||
|
|
||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
||||
return { |
return { |
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CurrentRateService: jest.fn().mockImplementation(() => { |
CurrentRateService: jest.fn().mockImplementation(() => { |
||||
return CurrentRateServiceMock; |
return CurrentRateServiceMock; |
||||
}) |
}) |
||||
}; |
}; |
||||
}); |
}); |
||||
|
|
||||
jest.mock( |
jest.mock( |
||||
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
||||
() => { |
() => { |
||||
return { |
return { |
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
||||
return PortfolioSnapshotServiceMock; |
return PortfolioSnapshotServiceMock; |
||||
}) |
}) |
||||
}; |
}; |
||||
} |
} |
||||
); |
); |
||||
|
|
||||
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
||||
return { |
return { |
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
RedisCacheService: jest.fn().mockImplementation(() => { |
RedisCacheService: jest.fn().mockImplementation(() => { |
||||
return RedisCacheServiceMock; |
return RedisCacheServiceMock; |
||||
}) |
}) |
||||
}; |
}; |
||||
}); |
}); |
||||
|
|
||||
describe('PortfolioCalculator', () => { |
describe('PortfolioCalculator', () => { |
||||
let activityDtos: CreateOrderDto[]; |
let activityDtos: CreateOrderDto[]; |
||||
|
|
||||
let configurationService: ConfigurationService; |
let configurationService: ConfigurationService; |
||||
let currentRateService: CurrentRateService; |
let currentRateService: CurrentRateService; |
||||
let exchangeRateDataService: ExchangeRateDataService; |
let exchangeRateDataService: ExchangeRateDataService; |
||||
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
let portfolioSnapshotService: PortfolioSnapshotService; |
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
let redisCacheService: RedisCacheService; |
let redisCacheService: RedisCacheService; |
||||
|
|
||||
beforeAll(() => { |
beforeAll(() => { |
||||
activityDtos = loadActivityExportFile( |
activityDtos = loadActivityExportFile( |
||||
join( |
join( |
||||
__dirname, |
__dirname, |
||||
'../../../../../../../test/import/ok-novn-buy-and-sell.json' |
'../../../../../../../test/import/ok-novn-buy-and-sell.json' |
||||
) |
) |
||||
); |
); |
||||
}); |
}); |
||||
|
|
||||
beforeEach(() => { |
beforeEach(() => { |
||||
configurationService = new ConfigurationService(); |
configurationService = new ConfigurationService(); |
||||
|
|
||||
currentRateService = new CurrentRateService(null, null, null, null); |
currentRateService = new CurrentRateService(null, null, null, null); |
||||
|
|
||||
exchangeRateDataService = new ExchangeRateDataService( |
exchangeRateDataService = new ExchangeRateDataService( |
||||
null, |
null, |
||||
null, |
null, |
||||
null, |
null, |
||||
null |
null |
||||
); |
); |
||||
|
|
||||
portfolioSnapshotService = new PortfolioSnapshotService(null); |
portfolioSnapshotService = new PortfolioSnapshotService(null); |
||||
|
|
||||
redisCacheService = new RedisCacheService(null, null); |
redisCacheService = new RedisCacheService(null, null); |
||||
|
|
||||
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
||||
configurationService, |
configurationService, |
||||
currentRateService, |
currentRateService, |
||||
exchangeRateDataService, |
exchangeRateDataService, |
||||
portfolioSnapshotService, |
portfolioSnapshotService, |
||||
redisCacheService |
redisCacheService |
||||
); |
); |
||||
}); |
}); |
||||
|
|
||||
describe('get current positions', () => { |
describe('get current positions', () => { |
||||
it.only('with NOVN.SW buy and sell', async () => { |
it.only('with NOVN.SW buy and sell', async () => { |
||||
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); |
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); |
||||
|
|
||||
const activities: Activity[] = activityDtos.map((activity) => ({ |
const activities: Activity[] = activityDtos.map((activity) => ({ |
||||
...activityDummyData, |
...activityDummyData, |
||||
...activity, |
...activity, |
||||
date: parseDate(activity.date), |
date: parseDate(activity.date), |
||||
SymbolProfile: { |
SymbolProfile: { |
||||
...symbolProfileDummyData, |
...symbolProfileDummyData, |
||||
currency: activity.currency, |
currency: activity.currency, |
||||
dataSource: activity.dataSource, |
dataSource: activity.dataSource, |
||||
name: 'Novartis AG', |
name: 'Novartis AG', |
||||
symbol: activity.symbol |
symbol: activity.symbol |
||||
} |
} |
||||
})); |
})); |
||||
|
|
||||
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
activities, |
activities, |
||||
calculationType: PerformanceCalculationType.ROAI, |
calculationType: PerformanceCalculationType.ROAI, |
||||
currency: 'CHF', |
currency: 'CHF', |
||||
userId: userDummyData.id |
userId: userDummyData.id |
||||
}); |
}); |
||||
|
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
const investments = portfolioCalculator.getInvestments(); |
const investments = portfolioCalculator.getInvestments(); |
||||
|
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ |
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ |
||||
data: portfolioSnapshot.historicalData, |
data: portfolioSnapshot.historicalData, |
||||
groupBy: 'month' |
groupBy: 'month' |
||||
}); |
}); |
||||
|
|
||||
expect(portfolioSnapshot.historicalData[0]).toEqual({ |
expect(portfolioSnapshot.historicalData[0]).toEqual({ |
||||
date: '2022-03-06', |
date: '2022-03-06', |
||||
investmentValueWithCurrencyEffect: 0, |
investmentValueWithCurrencyEffect: 0, |
||||
netPerformance: 0, |
netPerformance: 0, |
||||
netPerformanceInPercentage: 0, |
netPerformanceInPercentage: 0, |
||||
netPerformanceInPercentageWithCurrencyEffect: 0, |
netPerformanceInPercentageWithCurrencyEffect: 0, |
||||
netPerformanceWithCurrencyEffect: 0, |
netPerformanceWithCurrencyEffect: 0, |
||||
netWorth: 0, |
netWorth: 0, |
||||
totalAccountBalance: 0, |
totalAccountBalance: 0, |
||||
totalInvestment: 0, |
totalInvestment: 0, |
||||
totalInvestmentValueWithCurrencyEffect: 0, |
totalInvestmentValueWithCurrencyEffect: 0, |
||||
value: 0, |
value: 0, |
||||
valueWithCurrencyEffect: 0 |
valueWithCurrencyEffect: 0 |
||||
}); |
}); |
||||
|
|
||||
expect(portfolioSnapshot.historicalData[1]).toEqual({ |
expect(portfolioSnapshot.historicalData[1]).toEqual({ |
||||
date: '2022-03-07', |
date: '2022-03-07', |
||||
investmentValueWithCurrencyEffect: 151.6, |
investmentValueWithCurrencyEffect: 151.6, |
||||
netPerformance: 0, |
netPerformance: 0, |
||||
netPerformanceInPercentage: 0, |
netPerformanceInPercentage: 0, |
||||
netPerformanceInPercentageWithCurrencyEffect: 0, |
netPerformanceInPercentageWithCurrencyEffect: 0, |
||||
netPerformanceWithCurrencyEffect: 0, |
netPerformanceWithCurrencyEffect: 0, |
||||
netWorth: 151.6, |
netWorth: 151.6, |
||||
totalAccountBalance: 0, |
totalAccountBalance: 0, |
||||
totalInvestment: 151.6, |
totalInvestment: 151.6, |
||||
totalInvestmentValueWithCurrencyEffect: 151.6, |
totalInvestmentValueWithCurrencyEffect: 151.6, |
||||
value: 151.6, |
value: 151.6, |
||||
valueWithCurrencyEffect: 151.6 |
valueWithCurrencyEffect: 151.6 |
||||
}); |
}); |
||||
|
|
||||
expect( |
expect( |
||||
portfolioSnapshot.historicalData[ |
portfolioSnapshot.historicalData[ |
||||
portfolioSnapshot.historicalData.length - 1 |
portfolioSnapshot.historicalData.length - 1 |
||||
] |
] |
||||
).toEqual({ |
).toEqual({ |
||||
date: '2022-04-11', |
date: '2022-04-11', |
||||
investmentValueWithCurrencyEffect: 0, |
investmentValueWithCurrencyEffect: 0, |
||||
netPerformance: 19.86, |
netPerformance: 19.86, |
||||
netPerformanceInPercentage: 0.13100263852242744, |
netPerformanceInPercentage: 0.13100263852242744, |
||||
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, |
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, |
||||
netPerformanceWithCurrencyEffect: 19.86, |
netPerformanceWithCurrencyEffect: 19.86, |
||||
netWorth: 0, |
netWorth: 0, |
||||
totalAccountBalance: 0, |
totalAccountBalance: 0, |
||||
totalInvestment: 0, |
totalInvestment: 0, |
||||
totalInvestmentValueWithCurrencyEffect: 0, |
totalInvestmentValueWithCurrencyEffect: 0, |
||||
value: 0, |
value: 0, |
||||
valueWithCurrencyEffect: 0 |
valueWithCurrencyEffect: 0 |
||||
}); |
}); |
||||
|
|
||||
expect(portfolioSnapshot).toMatchObject({ |
expect(portfolioSnapshot).toMatchObject({ |
||||
currentValueInBaseCurrency: new Big('0'), |
currentValueInBaseCurrency: new Big('0'), |
||||
errors: [], |
errors: [], |
||||
hasErrors: false, |
hasErrors: false, |
||||
positions: [ |
positions: [ |
||||
{ |
{ |
||||
averagePrice: new Big('0'), |
averagePrice: new Big('0'), |
||||
currency: 'CHF', |
currency: 'CHF', |
||||
dataSource: 'YAHOO', |
dataSource: 'YAHOO', |
||||
dividend: new Big('0'), |
dividend: new Big('0'), |
||||
dividendInBaseCurrency: new Big('0'), |
dividendInBaseCurrency: new Big('0'), |
||||
fee: new Big('0'), |
fee: new Big('0'), |
||||
feeInBaseCurrency: new Big('0'), |
feeInBaseCurrency: new Big('0'), |
||||
firstBuyDate: '2022-03-07', |
firstBuyDate: '2022-03-07', |
||||
grossPerformance: new Big('19.86'), |
grossPerformance: new Big('19.86'), |
||||
grossPerformancePercentage: new Big('0.13100263852242744063'), |
grossPerformancePercentage: new Big('0.13100263852242744063'), |
||||
grossPerformancePercentageWithCurrencyEffect: new Big( |
grossPerformancePercentageWithCurrencyEffect: new Big( |
||||
'0.13100263852242744063' |
'0.13100263852242744063' |
||||
), |
), |
||||
grossPerformanceWithCurrencyEffect: new Big('19.86'), |
grossPerformanceWithCurrencyEffect: new Big('19.86'), |
||||
investment: new Big('0'), |
investment: new Big('0'), |
||||
investmentWithCurrencyEffect: new Big('0'), |
investmentWithCurrencyEffect: new Big('0'), |
||||
netPerformance: new Big('19.86'), |
netPerformance: new Big('19.86'), |
||||
netPerformancePercentage: new Big('0.13100263852242744063'), |
netPerformancePercentage: new Big('0.13100263852242744063'), |
||||
netPerformancePercentageWithCurrencyEffectMap: { |
netPerformancePercentageWithCurrencyEffectMap: { |
||||
max: new Big('0.13100263852242744063') |
max: new Big('0.13100263852242744063') |
||||
}, |
}, |
||||
netPerformanceWithCurrencyEffectMap: { |
netPerformanceWithCurrencyEffectMap: { |
||||
max: new Big('19.86') |
max: new Big('19.86') |
||||
}, |
}, |
||||
marketPrice: 87.8, |
marketPrice: 87.8, |
||||
marketPriceInBaseCurrency: 87.8, |
marketPriceInBaseCurrency: 87.8, |
||||
quantity: new Big('0'), |
quantity: new Big('0'), |
||||
symbol: 'NOVN.SW', |
symbol: 'NOVN.SW', |
||||
tags: [], |
tags: [], |
||||
timeWeightedInvestment: new Big('151.6'), |
timeWeightedInvestment: new Big('151.6'), |
||||
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), |
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), |
||||
transactionCount: 2, |
transactionCount: 2, |
||||
valueInBaseCurrency: new Big('0') |
valueInBaseCurrency: new Big('0') |
||||
} |
} |
||||
], |
], |
||||
totalFeesWithCurrencyEffect: new Big('0'), |
totalFeesWithCurrencyEffect: new Big('0'), |
||||
totalInterestWithCurrencyEffect: new Big('0'), |
totalInterestWithCurrencyEffect: new Big('0'), |
||||
totalInvestment: new Big('0'), |
totalInvestment: new Big('0'), |
||||
totalInvestmentWithCurrencyEffect: new Big('0'), |
totalInvestmentWithCurrencyEffect: new Big('0'), |
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'), |
totalLiabilitiesWithCurrencyEffect: new Big('0'), |
||||
totalValuablesWithCurrencyEffect: new Big('0') |
totalValuablesWithCurrencyEffect: new Big('0') |
||||
}); |
}); |
||||
|
|
||||
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( |
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( |
||||
expect.objectContaining({ |
expect.objectContaining({ |
||||
netPerformance: 19.86, |
netPerformance: 19.86, |
||||
netPerformanceInPercentage: 0.13100263852242744063, |
netPerformanceInPercentage: 0.13100263852242744063, |
||||
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, |
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, |
||||
netPerformanceWithCurrencyEffect: 19.86, |
netPerformanceWithCurrencyEffect: 19.86, |
||||
totalInvestmentValueWithCurrencyEffect: 0 |
totalInvestmentValueWithCurrencyEffect: 0 |
||||
}) |
}) |
||||
); |
); |
||||
|
|
||||
expect(investments).toEqual([ |
expect(investments).toEqual([ |
||||
{ date: '2022-03-07', investment: new Big('151.6') }, |
{ date: '2022-03-07', investment: new Big('151.6') }, |
||||
{ date: '2022-04-08', investment: new Big('0') } |
{ date: '2022-04-08', investment: new Big('0') } |
||||
]); |
]); |
||||
|
|
||||
expect(investmentsByMonth).toEqual([ |
expect(investmentsByMonth).toEqual([ |
||||
{ date: '2022-03-01', investment: 151.6 }, |
{ date: '2022-03-01', investment: 151.6 }, |
||||
{ date: '2022-04-01', investment: -151.6 } |
{ date: '2022-04-01', investment: -151.6 } |
||||
]); |
]); |
||||
}); |
}); |
||||
}); |
}); |
||||
}); |
}); |
||||
|
@ -0,0 +1,82 @@ |
|||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
||||
|
|
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { Prisma, Tag } from '@prisma/client'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class TagService { |
||||
|
public constructor(private readonly prismaService: PrismaService) {} |
||||
|
|
||||
|
public async createTag(data: Prisma.TagCreateInput) { |
||||
|
return this.prismaService.tag.create({ |
||||
|
data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> { |
||||
|
return this.prismaService.tag.delete({ where }); |
||||
|
} |
||||
|
|
||||
|
public async getTag( |
||||
|
tagWhereUniqueInput: Prisma.TagWhereUniqueInput |
||||
|
): Promise<Tag> { |
||||
|
return this.prismaService.tag.findUnique({ |
||||
|
where: tagWhereUniqueInput |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async getTags({ |
||||
|
cursor, |
||||
|
orderBy, |
||||
|
skip, |
||||
|
take, |
||||
|
where |
||||
|
}: { |
||||
|
cursor?: Prisma.TagWhereUniqueInput; |
||||
|
orderBy?: Prisma.TagOrderByWithRelationInput; |
||||
|
skip?: number; |
||||
|
take?: number; |
||||
|
where?: Prisma.TagWhereInput; |
||||
|
} = {}) { |
||||
|
return this.prismaService.tag.findMany({ |
||||
|
cursor, |
||||
|
orderBy, |
||||
|
skip, |
||||
|
take, |
||||
|
where |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async getTagsWithActivityCount() { |
||||
|
const tagsWithOrderCount = await this.prismaService.tag.findMany({ |
||||
|
include: { |
||||
|
_count: { |
||||
|
select: { orders: true, symbolProfile: true } |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return tagsWithOrderCount.map(({ _count, id, name, userId }) => { |
||||
|
return { |
||||
|
id, |
||||
|
name, |
||||
|
userId, |
||||
|
activityCount: _count.orders, |
||||
|
holdingCount: _count.symbolProfile |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async updateTag({ |
||||
|
data, |
||||
|
where |
||||
|
}: { |
||||
|
data: Prisma.TagUpdateInput; |
||||
|
where: Prisma.TagWhereUniqueInput; |
||||
|
}): Promise<Tag> { |
||||
|
return this.prismaService.tag.update({ |
||||
|
data, |
||||
|
where |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
import { resetHours } from '@ghostfolio/common/helper'; |
||||
|
|
||||
|
import { addDays } from 'date-fns'; |
||||
|
|
||||
|
import { DateQuery } from '../app/portfolio/interfaces/date-query.interface'; |
||||
|
|
||||
|
export class DateQueryHelper { |
||||
|
public handleDateQueryIn(dateQuery: DateQuery): { |
||||
|
query: DateQuery; |
||||
|
dates: Date[]; |
||||
|
} { |
||||
|
let dates = []; |
||||
|
let query = dateQuery; |
||||
|
if (dateQuery.in?.length > 0) { |
||||
|
dates = dateQuery.in; |
||||
|
const end = Math.max(...dates.map((d) => d.getTime())); |
||||
|
const start = Math.min(...dates.map((d) => d.getTime())); |
||||
|
query = { |
||||
|
gte: resetHours(new Date(start)), |
||||
|
lt: resetHours(addDays(end, 1)) |
||||
|
}; |
||||
|
} |
||||
|
return { query, dates }; |
||||
|
} |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
||||
|
|
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { SymbolProfileOverwriteService } from './symbol-profile-overwrite.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [PrismaModule], |
||||
|
providers: [SymbolProfileOverwriteService], |
||||
|
exports: [SymbolProfileOverwriteService] |
||||
|
}) |
||||
|
export class SymbolProfileOverwriteModule {} |
@ -0,0 +1,69 @@ |
|||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
||||
|
|
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { DataSource, Prisma, SymbolProfileOverrides } from '@prisma/client'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class SymbolProfileOverwriteService { |
||||
|
public constructor(private readonly prismaService: PrismaService) {} |
||||
|
|
||||
|
public async add( |
||||
|
assetProfileOverwrite: Prisma.SymbolProfileOverridesCreateInput |
||||
|
): Promise<SymbolProfileOverrides | never> { |
||||
|
return this.prismaService.symbolProfileOverrides.create({ |
||||
|
data: assetProfileOverwrite |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async delete(symbolProfileId: string) { |
||||
|
return this.prismaService.symbolProfileOverrides.delete({ |
||||
|
where: { symbolProfileId: symbolProfileId } |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public updateSymbolProfileOverrides({ |
||||
|
assetClass, |
||||
|
assetSubClass, |
||||
|
name, |
||||
|
countries, |
||||
|
sectors, |
||||
|
url, |
||||
|
symbolProfileId |
||||
|
}: Prisma.SymbolProfileOverridesUpdateInput & { symbolProfileId: string }) { |
||||
|
return this.prismaService.symbolProfileOverrides.update({ |
||||
|
data: { |
||||
|
assetClass, |
||||
|
assetSubClass, |
||||
|
name, |
||||
|
countries, |
||||
|
sectors, |
||||
|
url |
||||
|
}, |
||||
|
where: { symbolProfileId: symbolProfileId } |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async GetSymbolProfileId( |
||||
|
Symbol: string, |
||||
|
datasource: DataSource |
||||
|
): Promise<string> { |
||||
|
const SymbolProfileId = await this.prismaService.symbolProfile |
||||
|
.findFirst({ |
||||
|
where: { |
||||
|
symbol: Symbol, |
||||
|
dataSource: datasource |
||||
|
} |
||||
|
}) |
||||
|
.then((s) => s.id); |
||||
|
|
||||
|
const symbolProfileIdSaved = await this.prismaService.symbolProfileOverrides |
||||
|
.findFirst({ |
||||
|
where: { |
||||
|
symbolProfileId: SymbolProfileId |
||||
|
} |
||||
|
}) |
||||
|
.then((s) => s?.symbolProfileId); |
||||
|
|
||||
|
return symbolProfileIdSaved; |
||||
|
} |
||||
|
} |
@ -0,0 +1,46 @@ |
|||||
|
import { Prisma, PrismaClient } from '@prisma/client'; |
||||
|
|
||||
|
class Chunk<T> implements Iterable<T[] | undefined> { |
||||
|
protected constructor( |
||||
|
private readonly values: readonly T[], |
||||
|
private readonly size: number |
||||
|
) {} |
||||
|
|
||||
|
*[Symbol.iterator]() { |
||||
|
const copy = [...this.values]; |
||||
|
if (copy.length === 0) yield undefined; |
||||
|
while (copy.length) yield copy.splice(0, this.size); |
||||
|
} |
||||
|
|
||||
|
map<U>(mapper: (items?: T[]) => U): U[] { |
||||
|
return Array.from(this).map((items) => mapper(items)); |
||||
|
} |
||||
|
|
||||
|
static of<U>(values: readonly U[]) { |
||||
|
return { |
||||
|
by: (size: number) => new Chunk(values, size) |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export type Queryable<T, Result> = ( |
||||
|
p: PrismaClient, |
||||
|
vs?: T[] |
||||
|
) => Prisma.PrismaPromise<Result>; |
||||
|
export class BatchPrismaClient { |
||||
|
constructor( |
||||
|
private readonly prisma: PrismaClient, |
||||
|
private readonly size = 32_000 |
||||
|
) {} |
||||
|
|
||||
|
over<T>(values: readonly T[]) { |
||||
|
return { |
||||
|
with: <Result>(queryable: Queryable<T, Result>) => |
||||
|
this.prisma.$transaction( |
||||
|
Chunk.of(values) |
||||
|
.by(this.size) |
||||
|
.map((vs) => queryable(this.prisma, vs)) |
||||
|
) |
||||
|
}; |
||||
|
} |
||||
|
} |
@ -1,9 +1,12 @@ |
|||||
export type DateRange = |
export type DateRange = |
||||
| '1d' |
| '1d' |
||||
|
| 'wtd' |
||||
|
| '1w' |
||||
|
| 'mtd' |
||||
|
| '1m' |
||||
|
| '3m' |
||||
|
| 'ytd' |
||||
| '1y' |
| '1y' |
||||
| '5y' |
| '5y' |
||||
| 'max' |
| 'max' |
||||
| 'mtd' |
|
||||
| 'wtd' |
|
||||
| 'ytd' |
|
||||
| string; // '2024', '2023', '2022', etc.
|
| string; // '2024', '2023', '2022', etc.
|
||||
|
@ -1,3 +1,54 @@ |
|||||
:host { |
:host { |
||||
display: block; |
display: block; |
||||
|
.activities { |
||||
|
overflow-x: auto; |
||||
|
.mat-mdc-table { |
||||
|
th { |
||||
|
::ng-deep { |
||||
|
.mat-sort-header-container { |
||||
|
justify-content: inherit; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.mat-mdc-row { |
||||
|
.type-badge { |
||||
|
background-color: rgba(var(--palette-foreground-text), 0.05); |
||||
|
border-radius: 1rem; |
||||
|
line-height: 1em; |
||||
|
ion-icon { |
||||
|
font-size: 1rem; |
||||
|
} |
||||
|
&.buy { |
||||
|
color: var(--green); |
||||
|
} |
||||
|
&.dividend { |
||||
|
color: var(--blue); |
||||
|
} |
||||
|
&.stake { |
||||
|
color: var(--blue); |
||||
|
} |
||||
|
&.item { |
||||
|
color: var(--purple); |
||||
|
} |
||||
|
&.liability { |
||||
|
color: var(--red); |
||||
|
} |
||||
|
&.sell { |
||||
|
color: var(--orange); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host-context(.is-dark-theme) { |
||||
|
.mat-mdc-table { |
||||
|
.type-badge { |
||||
|
background-color: rgba( |
||||
|
var(--palette-foreground-text-dark), |
||||
|
0.1 |
||||
|
) !important; |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
|
@ -0,0 +1,185 @@ |
|||||
|
{ |
||||
|
"migrations": [ |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.3.0-beta.6", |
||||
|
"description": "Updates the nx wrapper.", |
||||
|
"implementation": "./src/migrations/update-17-3-0/update-nxw", |
||||
|
"package": "nx", |
||||
|
"name": "17.3.0-update-nx-wrapper" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "18.0.0-beta.2", |
||||
|
"description": "Updates nx.json to disabled adding plugins when generating projects in an existing Nx workspace", |
||||
|
"implementation": "./src/migrations/update-18-0-0/disable-crystal-for-existing-workspaces", |
||||
|
"x-repair-skip": true, |
||||
|
"package": "nx", |
||||
|
"name": "18.0.0-disable-adding-plugins-for-existing-workspaces" |
||||
|
}, |
||||
|
{ |
||||
|
"version": "18.1.0-beta.3", |
||||
|
"description": "Moves affected.defaultBase to defaultBase in `nx.json`", |
||||
|
"implementation": "./src/migrations/update-17-2-0/move-default-base", |
||||
|
"package": "nx", |
||||
|
"name": "move-default-base-to-nx-json-root" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "18.1.0-beta.3", |
||||
|
"description": "Update to Cypress ^13.6.6 if the workspace is using Cypress v13 to ensure workspaces don't use v13.6.5 which has an issue when verifying Cypress.", |
||||
|
"implementation": "./src/migrations/update-18-1-0/update-cypress-version-13-6-6", |
||||
|
"package": "@nx/cypress", |
||||
|
"name": "update-cypress-version-13-6-6" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.2.6-beta.1", |
||||
|
"description": "Rename workspace rules from @nx/workspace/name to @nx/workspace-name", |
||||
|
"implementation": "./src/migrations/update-17-2-6-rename-workspace-rules/rename-workspace-rules", |
||||
|
"package": "@nx/eslint-plugin", |
||||
|
"name": "update-17-2-6-rename-workspace-rules" |
||||
|
}, |
||||
|
{ |
||||
|
"version": "17.1.0-beta.2", |
||||
|
"description": "Move jest executor options to nx.json targetDefaults", |
||||
|
"implementation": "./src/migrations/update-17-1-0/move-options-to-target-defaults", |
||||
|
"package": "@nx/jest", |
||||
|
"name": "move-options-to-target-defaults" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.1.0-beta.5", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.0.0" |
||||
|
}, |
||||
|
"description": "Update the @angular/cli package version to ~17.0.0.", |
||||
|
"factory": "./src/migrations/update-17-1-0/update-angular-cli", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "update-angular-cli-version-17-0-0" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.1.0-beta.5", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.0.0" |
||||
|
}, |
||||
|
"description": "Rename 'browserTarget' to 'buildTarget'.", |
||||
|
"factory": "./src/migrations/update-17-1-0/browser-target-to-build-target", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "rename-browser-target-to-build-target" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.1.0-beta.5", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.0.0" |
||||
|
}, |
||||
|
"description": "Replace usages of '@nguniversal/builders' with '@angular-devkit/build-angular'.", |
||||
|
"factory": "./src/migrations/update-17-1-0/replace-nguniversal-builders", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "replace-nguniversal-builders" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.1.0-beta.5", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.0.0" |
||||
|
}, |
||||
|
"description": "Replace usages of '@nguniversal/' packages with '@angular/ssr'.", |
||||
|
"factory": "./src/migrations/update-17-1-0/replace-nguniversal-engines", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "replace-nguniversal-engines" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.1.0-beta.5", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.0.0" |
||||
|
}, |
||||
|
"description": "Replace the deep imports from 'zone.js/dist/zone' and 'zone.js/dist/zone-testing' with 'zone.js' and 'zone.js/testing'.", |
||||
|
"factory": "./src/migrations/update-17-1-0/update-zone-js-deep-import", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "update-zone-js-deep-import" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.2.0-beta.2", |
||||
|
"description": "Rename '@nx/angular:webpack-dev-server' executor to '@nx/angular:dev-server'", |
||||
|
"factory": "./src/migrations/update-17-2-0/rename-webpack-dev-server", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "rename-webpack-dev-server-executor" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.3.0-beta.10", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.1.0" |
||||
|
}, |
||||
|
"description": "Update the @angular/cli package version to ~17.1.0.", |
||||
|
"factory": "./src/migrations/update-17-3-0/update-angular-cli", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "update-angular-cli-version-17-1-0" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.3.0-beta.10", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.1.0" |
||||
|
}, |
||||
|
"description": "Add 'browser-sync' as dev dependency when '@angular-devkit/build-angular:ssr-dev-server' or '@nx/angular:module-federation-dev-ssr' is used.", |
||||
|
"factory": "./src/migrations/update-17-3-0/add-browser-sync-dependency", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "add-browser-sync-dependency" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "17.3.0-beta.10", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.1.0" |
||||
|
}, |
||||
|
"description": "Add 'autoprefixer' as dev dependency when '@nx/angular:ng-packagr-lite' or '@nx/angular:package` is used.", |
||||
|
"factory": "./src/migrations/update-17-3-0/add-autoprefixer-dependency", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "add-autoprefixer-dependency" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "18.0.0-beta.0", |
||||
|
"description": "Add NX_MF_DEV_SERVER_STATIC_REMOTES to inputs for task hashing when '@nx/angular:webpack-browser' is used for Module Federation.", |
||||
|
"factory": "./src/migrations/update-18-0-0/add-mf-env-var-to-target-defaults", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "add-module-federation-env-var-to-target-defaults" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "18.1.0-beta.1", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.2.0" |
||||
|
}, |
||||
|
"description": "Update the @angular/cli package version to ~17.2.0.", |
||||
|
"factory": "./src/migrations/update-18-1-0/update-angular-cli", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "update-angular-cli-version-17-2-0" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "18.1.1-beta.0", |
||||
|
"description": "Ensure targetDefaults inputs for task hashing when '@nx/angular:webpack-browser' is used are correct for Module Federation.", |
||||
|
"factory": "./src/migrations/update-18-1-1/fix-target-defaults-inputs", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "fix-target-defaults-for-webpack-browser" |
||||
|
}, |
||||
|
{ |
||||
|
"cli": "nx", |
||||
|
"version": "18.2.0-beta.0", |
||||
|
"requires": { |
||||
|
"@angular/core": ">=17.3.0" |
||||
|
}, |
||||
|
"description": "Update the @angular/cli package version to ~17.3.0.", |
||||
|
"factory": "./src/migrations/update-18-2-0/update-angular-cli", |
||||
|
"package": "@nx/angular", |
||||
|
"name": "update-angular-cli-version-17-3-0" |
||||
|
} |
||||
|
] |
||||
|
} |
File diff suppressed because it is too large
@ -0,0 +1,22 @@ |
|||||
|
-- AlterEnum |
||||
|
ALTER TYPE "Type" ADD VALUE IF NOT EXISTS 'STAKE'; |
||||
|
|
||||
|
-- CreateTable |
||||
|
CREATE TABLE IF NOT EXISTS "_SymbolProfileToTag" ( |
||||
|
"A" TEXT NOT NULL, |
||||
|
"B" TEXT NOT NULL |
||||
|
); |
||||
|
|
||||
|
-- CreateIndex |
||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "_SymbolProfileToTag_AB_unique" ON "_SymbolProfileToTag"("A", "B"); |
||||
|
|
||||
|
-- CreateIndex |
||||
|
CREATE INDEX IF NOT EXISTS "_SymbolProfileToTag_B_index" ON "_SymbolProfileToTag"("B"); |
||||
|
|
||||
|
-- AddForeignKey |
||||
|
ALTER TABLE "_SymbolProfileToTag" DROP CONSTRAINT IF EXISTS "_SymbolProfileToTag_A_fkey" ; |
||||
|
ALTER TABLE "_SymbolProfileToTag" ADD CONSTRAINT "_SymbolProfileToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "SymbolProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
||||
|
|
||||
|
-- AddForeignKey |
||||
|
ALTER TABLE "_SymbolProfileToTag" DROP CONSTRAINT IF EXISTS "_SymbolProfileToTag_B_fkey" ; |
||||
|
ALTER TABLE "_SymbolProfileToTag" ADD CONSTRAINT "_SymbolProfileToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
File diff suppressed because it is too large
Loading…
Reference in new issue