mirror of https://github.com/ghostfolio/ghostfolio
28 changed files with 1074 additions and 10 deletions
@ -0,0 +1,188 @@ |
|||||
|
import { |
||||
|
activityDummyData, |
||||
|
loadExportFile, |
||||
|
symbolProfileDummyData, |
||||
|
userDummyData |
||||
|
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
||||
|
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
||||
|
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
||||
|
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
||||
|
import { applySplitAdjustments } from '@ghostfolio/api/helper/portfolio.helper'; |
||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
||||
|
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; |
||||
|
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
||||
|
|
||||
|
import { Big } from 'big.js'; |
||||
|
import { join } from 'node:path'; |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
||||
|
return { |
||||
|
CurrentRateService: jest.fn().mockImplementation(() => { |
||||
|
return CurrentRateServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
||||
|
() => { |
||||
|
return { |
||||
|
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
||||
|
return PortfolioSnapshotServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
||||
|
return { |
||||
|
RedisCacheService: jest.fn().mockImplementation(() => { |
||||
|
return RedisCacheServiceMock; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
describe('PortfolioCalculator', () => { |
||||
|
let exportResponse: ExportResponse; |
||||
|
|
||||
|
let configurationService: ConfigurationService; |
||||
|
let currentRateService: CurrentRateService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
||||
|
let portfolioSnapshotService: PortfolioSnapshotService; |
||||
|
let redisCacheService: RedisCacheService; |
||||
|
|
||||
|
beforeAll(() => { |
||||
|
exportResponse = loadExportFile( |
||||
|
join( |
||||
|
__dirname, |
||||
|
'../../../../../../../test/import/ok/nvda-buy-with-split-and-sell.json' |
||||
|
) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
configurationService = new ConfigurationService(); |
||||
|
|
||||
|
currentRateService = new CurrentRateService(null, null, null, null); |
||||
|
|
||||
|
exchangeRateDataService = new ExchangeRateDataService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
|
||||
|
portfolioSnapshotService = new PortfolioSnapshotService(null, null); |
||||
|
|
||||
|
redisCacheService = new RedisCacheService(null, null); |
||||
|
|
||||
|
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
||||
|
configurationService, |
||||
|
currentRateService, |
||||
|
exchangeRateDataService, |
||||
|
portfolioSnapshotService, |
||||
|
redisCacheService |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('get current positions', () => { |
||||
|
it.only('with NVDA buy (before a 10:1 split) and sell', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2024-06-14').getTime()); |
||||
|
|
||||
|
const activities: Activity[] = exportResponse.activities.map( |
||||
|
(activity) => ({ |
||||
|
...activityDummyData, |
||||
|
...activity, |
||||
|
date: parseDate(activity.date), |
||||
|
feeInAssetProfileCurrency: activity.fee, |
||||
|
feeInBaseCurrency: activity.fee, |
||||
|
SymbolProfile: { |
||||
|
...symbolProfileDummyData, |
||||
|
currency: activity.currency, |
||||
|
dataSource: activity.dataSource, |
||||
|
name: 'NVIDIA Corporation', |
||||
|
symbol: activity.symbol |
||||
|
}, |
||||
|
unitPriceInAssetProfileCurrency: activity.unitPrice |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
// Mirrors the production flow in
|
||||
|
// ActivitiesService.getActivitiesForPortfolioCalculator
|
||||
|
const adjustedActivities = applySplitAdjustments({ |
||||
|
activities, |
||||
|
splits: [ |
||||
|
{ |
||||
|
dataSource: 'YAHOO', |
||||
|
date: parseDate('2024-06-10'), |
||||
|
factor: 10, |
||||
|
symbol: 'NVDA' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
||||
|
activities: adjustedActivities, |
||||
|
calculationType: PerformanceCalculationType.ROAI, |
||||
|
currency: exportResponse.user.settings.currency, |
||||
|
userId: userDummyData.id |
||||
|
}); |
||||
|
|
||||
|
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
||||
|
|
||||
|
const investments = portfolioCalculator.getInvestments(); |
||||
|
|
||||
|
expect(portfolioSnapshot).toMatchObject({ |
||||
|
currentValueInBaseCurrency: new Big('0'), |
||||
|
errors: [], |
||||
|
hasErrors: false, |
||||
|
positions: [ |
||||
|
{ |
||||
|
activitiesCount: 2, |
||||
|
averagePrice: new Big('0'), |
||||
|
currency: 'USD', |
||||
|
dataSource: 'YAHOO', |
||||
|
dateOfFirstActivity: '2024-06-03', |
||||
|
dividend: new Big('0'), |
||||
|
dividendInBaseCurrency: new Big('0'), |
||||
|
fee: new Big('0'), |
||||
|
feeInBaseCurrency: new Big('0'), |
||||
|
grossPerformance: new Big('50'), // 10 × (125 - 120) = 50
|
||||
|
grossPerformanceWithCurrencyEffect: new Big('50'), |
||||
|
investment: new Big('0'), |
||||
|
investmentWithCurrencyEffect: new Big('0'), |
||||
|
netPerformance: new Big('50'), |
||||
|
netPerformanceWithCurrencyEffectMap: { |
||||
|
max: new Big('50') |
||||
|
}, |
||||
|
marketPrice: 125, |
||||
|
marketPriceInBaseCurrency: 125, |
||||
|
quantity: new Big('0'), |
||||
|
symbol: 'NVDA', |
||||
|
tags: [], |
||||
|
timeWeightedInvestment: new Big('1200'), // 10 × 120 = 1200
|
||||
|
timeWeightedInvestmentWithCurrencyEffect: new Big('1200'), |
||||
|
valueInBaseCurrency: new Big('0') |
||||
|
} |
||||
|
], |
||||
|
totalFeesWithCurrencyEffect: new Big('0'), |
||||
|
totalInterestWithCurrencyEffect: new Big('0'), |
||||
|
totalInvestment: new Big('0'), |
||||
|
totalInvestmentWithCurrencyEffect: new Big('0'), |
||||
|
totalLiabilitiesWithCurrencyEffect: new Big('0') |
||||
|
}); |
||||
|
|
||||
|
expect(investments).toEqual([ |
||||
|
{ date: '2024-06-03', investment: new Big('1200') }, |
||||
|
{ date: '2024-06-14', investment: new Big('0') } |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,176 @@ |
|||||
|
import { DataSource } from '@prisma/client'; |
||||
|
|
||||
|
import { applySplitAdjustments } from './portfolio.helper'; |
||||
|
|
||||
|
describe('applySplitAdjustments', () => { |
||||
|
const symbolProfile = { |
||||
|
dataSource: DataSource.YAHOO, |
||||
|
symbol: 'NVDA' |
||||
|
}; |
||||
|
|
||||
|
const otherSymbolProfile = { |
||||
|
dataSource: DataSource.YAHOO, |
||||
|
symbol: 'AAPL' |
||||
|
}; |
||||
|
|
||||
|
const createActivity = ({ |
||||
|
date, |
||||
|
fee = 1, |
||||
|
quantity, |
||||
|
SymbolProfile = symbolProfile, |
||||
|
unitPrice |
||||
|
}: { |
||||
|
date: Date; |
||||
|
fee?: number; |
||||
|
quantity: number; |
||||
|
SymbolProfile?: typeof symbolProfile; |
||||
|
unitPrice: number; |
||||
|
}) => { |
||||
|
return { |
||||
|
date, |
||||
|
fee, |
||||
|
quantity, |
||||
|
SymbolProfile, |
||||
|
unitPrice, |
||||
|
unitPriceInAssetProfileCurrency: unitPrice |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
it('should return activities unchanged if there are no splits', () => { |
||||
|
const activities = [ |
||||
|
createActivity({ |
||||
|
date: new Date('2024-01-15'), |
||||
|
quantity: 10, |
||||
|
unitPrice: 400 |
||||
|
}) |
||||
|
]; |
||||
|
|
||||
|
expect(applySplitAdjustments({ activities, splits: [] })).toStrictEqual( |
||||
|
activities |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('should adjust an activity before a split', () => { |
||||
|
const activities = [ |
||||
|
createActivity({ |
||||
|
date: new Date('2024-01-15'), |
||||
|
quantity: 10, |
||||
|
unitPrice: 400 |
||||
|
}) |
||||
|
]; |
||||
|
|
||||
|
const [activity] = applySplitAdjustments({ |
||||
|
activities, |
||||
|
splits: [{ ...symbolProfile, date: new Date('2024-06-10'), factor: 4 }] |
||||
|
}); |
||||
|
|
||||
|
expect(activity.quantity).toBe(40); |
||||
|
expect(activity.unitPrice).toBe(100); |
||||
|
expect(activity.unitPriceInAssetProfileCurrency).toBe(100); |
||||
|
}); |
||||
|
|
||||
|
it('should keep the value and the fee unchanged', () => { |
||||
|
const activities = [ |
||||
|
createActivity({ |
||||
|
date: new Date('2024-01-15'), |
||||
|
fee: 7, |
||||
|
quantity: 3, |
||||
|
unitPrice: 700 |
||||
|
}) |
||||
|
]; |
||||
|
|
||||
|
const [activity] = applySplitAdjustments({ |
||||
|
activities, |
||||
|
splits: [{ ...symbolProfile, date: new Date('2024-06-10'), factor: 4 }] |
||||
|
}); |
||||
|
|
||||
|
expect(activity.quantity * activity.unitPrice).toBe(3 * 700); |
||||
|
expect(activity.fee).toBe(7); |
||||
|
}); |
||||
|
|
||||
|
it('should not adjust an activity on or after the split date', () => { |
||||
|
const activities = [ |
||||
|
createActivity({ |
||||
|
date: new Date('2024-06-10'), |
||||
|
quantity: 10, |
||||
|
unitPrice: 100 |
||||
|
}), |
||||
|
createActivity({ |
||||
|
date: new Date('2024-07-01'), |
||||
|
quantity: 20, |
||||
|
unitPrice: 110 |
||||
|
}) |
||||
|
]; |
||||
|
|
||||
|
const adjustedActivities = applySplitAdjustments({ |
||||
|
activities, |
||||
|
splits: [{ ...symbolProfile, date: new Date('2024-06-10'), factor: 4 }] |
||||
|
}); |
||||
|
|
||||
|
expect(adjustedActivities).toStrictEqual(activities); |
||||
|
}); |
||||
|
|
||||
|
it('should adjust for a reverse split', () => { |
||||
|
const activities = [ |
||||
|
createActivity({ |
||||
|
date: new Date('2024-01-15'), |
||||
|
quantity: 100, |
||||
|
unitPrice: 2 |
||||
|
}) |
||||
|
]; |
||||
|
|
||||
|
const [activity] = applySplitAdjustments({ |
||||
|
activities, |
||||
|
splits: [{ ...symbolProfile, date: new Date('2024-06-10'), factor: 0.1 }] |
||||
|
}); |
||||
|
|
||||
|
expect(activity.quantity).toBe(10); |
||||
|
expect(activity.unitPrice).toBe(20); |
||||
|
}); |
||||
|
|
||||
|
it('should compound multiple splits', () => { |
||||
|
const activities = [ |
||||
|
createActivity({ |
||||
|
date: new Date('2023-01-15'), |
||||
|
quantity: 10, |
||||
|
unitPrice: 600 |
||||
|
}), |
||||
|
createActivity({ |
||||
|
date: new Date('2024-01-15'), |
||||
|
quantity: 10, |
||||
|
unitPrice: 300 |
||||
|
}) |
||||
|
]; |
||||
|
|
||||
|
const adjustedActivities = applySplitAdjustments({ |
||||
|
activities, |
||||
|
splits: [ |
||||
|
{ ...symbolProfile, date: new Date('2023-06-10'), factor: 2 }, |
||||
|
{ ...symbolProfile, date: new Date('2024-06-10'), factor: 3 } |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
expect(adjustedActivities[0].quantity).toBe(60); |
||||
|
expect(adjustedActivities[0].unitPrice).toBe(100); |
||||
|
expect(adjustedActivities[1].quantity).toBe(30); |
||||
|
expect(adjustedActivities[1].unitPrice).toBe(100); |
||||
|
}); |
||||
|
|
||||
|
it('should not adjust activities of other symbols', () => { |
||||
|
const activities = [ |
||||
|
createActivity({ |
||||
|
date: new Date('2024-01-15'), |
||||
|
quantity: 10, |
||||
|
SymbolProfile: otherSymbolProfile, |
||||
|
unitPrice: 190 |
||||
|
}) |
||||
|
]; |
||||
|
|
||||
|
const adjustedActivities = applySplitAdjustments({ |
||||
|
activities, |
||||
|
splits: [{ ...symbolProfile, date: new Date('2024-06-10'), factor: 4 }] |
||||
|
}); |
||||
|
|
||||
|
expect(adjustedActivities).toStrictEqual(activities); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,12 @@ |
|||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
||||
|
|
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { AssetProfileSplitService } from './asset-profile-split.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
exports: [AssetProfileSplitService], |
||||
|
imports: [PrismaModule], |
||||
|
providers: [AssetProfileSplitService] |
||||
|
}) |
||||
|
export class AssetProfileSplitModule {} |
||||
@ -0,0 +1,104 @@ |
|||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
||||
|
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { AssetProfileSplit } from '@prisma/client'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AssetProfileSplitService { |
||||
|
public constructor(private readonly prismaService: PrismaService) {} |
||||
|
|
||||
|
public async deleteById({ |
||||
|
dataSource, |
||||
|
id, |
||||
|
symbol |
||||
|
}: AssetProfileIdentifier & { id: string }) { |
||||
|
return this.prismaService.assetProfileSplit.deleteMany({ |
||||
|
where: { |
||||
|
dataSource, |
||||
|
id, |
||||
|
symbol |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async deleteMany({ dataSource, symbol }: AssetProfileIdentifier) { |
||||
|
return this.prismaService.assetProfileSplit.deleteMany({ |
||||
|
where: { |
||||
|
dataSource, |
||||
|
symbol |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async getSplits({ |
||||
|
assetProfileIdentifiers |
||||
|
}: { |
||||
|
assetProfileIdentifiers: AssetProfileIdentifier[]; |
||||
|
}): Promise<AssetProfileSplit[]> { |
||||
|
if (assetProfileIdentifiers.length === 0) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
return this.prismaService.assetProfileSplit.findMany({ |
||||
|
orderBy: [ |
||||
|
{ |
||||
|
date: 'asc' |
||||
|
} |
||||
|
], |
||||
|
where: { |
||||
|
OR: assetProfileIdentifiers.map(({ dataSource, symbol }) => { |
||||
|
return { |
||||
|
dataSource, |
||||
|
symbol |
||||
|
}; |
||||
|
}) |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async upsert({ |
||||
|
dataSource, |
||||
|
date, |
||||
|
factor, |
||||
|
symbol |
||||
|
}: AssetProfileIdentifier & { |
||||
|
date: Date; |
||||
|
factor: number; |
||||
|
}): Promise<AssetProfileSplit> { |
||||
|
return this.prismaService.assetProfileSplit.upsert({ |
||||
|
create: { |
||||
|
dataSource, |
||||
|
date, |
||||
|
factor, |
||||
|
symbol |
||||
|
}, |
||||
|
update: { |
||||
|
factor |
||||
|
}, |
||||
|
where: { |
||||
|
dataSource_date_symbol: { |
||||
|
dataSource, |
||||
|
date, |
||||
|
symbol |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async updateAssetProfileIdentifier( |
||||
|
oldAssetProfileIdentifier: AssetProfileIdentifier, |
||||
|
newAssetProfileIdentifier: AssetProfileIdentifier |
||||
|
) { |
||||
|
return this.prismaService.assetProfileSplit.updateMany({ |
||||
|
data: { |
||||
|
dataSource: newAssetProfileIdentifier.dataSource, |
||||
|
symbol: newAssetProfileIdentifier.symbol |
||||
|
}, |
||||
|
where: { |
||||
|
dataSource: oldAssetProfileIdentifier.dataSource, |
||||
|
symbol: oldAssetProfileIdentifier.symbol |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
import { IsISO8601, IsNumber, Validate } from 'class-validator'; |
||||
|
|
||||
|
import { IsSplitFactorConstraint } from '../validator-constraints/is-split-factor'; |
||||
|
|
||||
|
export class CreateAssetProfileSplitDto { |
||||
|
@IsISO8601() |
||||
|
date: string; |
||||
|
|
||||
|
@IsNumber() |
||||
|
@Validate(IsSplitFactorConstraint) |
||||
|
factor: number; |
||||
|
} |
||||
@ -1,8 +1,9 @@ |
|||||
import { MarketData } from '@prisma/client'; |
import { AssetProfileSplit, MarketData } from '@prisma/client'; |
||||
|
|
||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; |
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; |
||||
|
|
||||
export interface AdminMarketDataDetails { |
export interface AdminMarketDataDetails { |
||||
assetProfile: Partial<EnhancedSymbolProfile>; |
assetProfile: Partial<EnhancedSymbolProfile>; |
||||
marketData: MarketData[]; |
marketData: MarketData[]; |
||||
|
splits: AssetProfileSplit[]; |
||||
} |
} |
||||
|
|||||
@ -1,8 +1,9 @@ |
|||||
import { MarketData } from '@prisma/client'; |
import { AssetProfileSplit, MarketData } from '@prisma/client'; |
||||
|
|
||||
import { EnhancedSymbolProfile } from '../enhanced-symbol-profile.interface'; |
import { EnhancedSymbolProfile } from '../enhanced-symbol-profile.interface'; |
||||
|
|
||||
export interface AssetProfileResponse { |
export interface AssetProfileResponse { |
||||
assetProfile: Partial<EnhancedSymbolProfile>; |
assetProfile: Partial<EnhancedSymbolProfile>; |
||||
marketData: MarketData[]; |
marketData: MarketData[]; |
||||
|
splits: AssetProfileSplit[]; |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,15 @@ |
|||||
|
import { |
||||
|
ValidatorConstraint, |
||||
|
ValidatorConstraintInterface |
||||
|
} from 'class-validator'; |
||||
|
|
||||
|
@ValidatorConstraint({ name: 'isSplitFactor' }) |
||||
|
export class IsSplitFactorConstraint implements ValidatorConstraintInterface { |
||||
|
public defaultMessage() { |
||||
|
return 'factor must be a positive number other than 1'; |
||||
|
} |
||||
|
|
||||
|
public validate(aFactor: number) { |
||||
|
return typeof aFactor === 'number' && aFactor > 0 && aFactor !== 1; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
-- CreateTable |
||||
|
CREATE TABLE "public"."AssetProfileSplit" ( |
||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
"dataSource" "public"."DataSource" NOT NULL, |
||||
|
"date" TIMESTAMP(3) NOT NULL, |
||||
|
"factor" DOUBLE PRECISION NOT NULL, |
||||
|
"id" TEXT NOT NULL, |
||||
|
"symbol" TEXT NOT NULL, |
||||
|
"updatedAt" TIMESTAMP(3) NOT NULL, |
||||
|
|
||||
|
CONSTRAINT "AssetProfileSplit_pkey" PRIMARY KEY ("id") |
||||
|
); |
||||
|
|
||||
|
-- CreateIndex |
||||
|
CREATE UNIQUE INDEX "AssetProfileSplit_dataSource_date_symbol_key" ON "public"."AssetProfileSplit"("dataSource", "date", "symbol"); |
||||
|
|
||||
|
-- CreateIndex |
||||
|
CREATE INDEX "AssetProfileSplit_dataSource_symbol_idx" ON "public"."AssetProfileSplit"("dataSource", "symbol"); |
||||
|
|
||||
|
-- CreateIndex |
||||
|
CREATE INDEX "AssetProfileSplit_date_idx" ON "public"."AssetProfileSplit"("date"); |
||||
@ -0,0 +1,34 @@ |
|||||
|
{ |
||||
|
"meta": { |
||||
|
"date": "2024-06-14T21:28:05.857Z", |
||||
|
"version": "dev" |
||||
|
}, |
||||
|
"activities": [ |
||||
|
{ |
||||
|
"fee": 0, |
||||
|
"quantity": 10, |
||||
|
"type": "SELL", |
||||
|
"unitPrice": 125, |
||||
|
"currency": "USD", |
||||
|
"dataSource": "YAHOO", |
||||
|
"date": "2024-06-14T00:00:00.000Z", |
||||
|
"symbol": "NVDA" |
||||
|
}, |
||||
|
{ |
||||
|
"fee": 0, |
||||
|
"quantity": 1, |
||||
|
"type": "BUY", |
||||
|
"unitPrice": 1200, |
||||
|
"currency": "USD", |
||||
|
"dataSource": "YAHOO", |
||||
|
"date": "2024-06-03T00:00:00.000Z", |
||||
|
"symbol": "NVDA" |
||||
|
} |
||||
|
], |
||||
|
"user": { |
||||
|
"settings": { |
||||
|
"currency": "USD", |
||||
|
"performanceCalculationType": "ROAI" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue