Browse Source

Merge remote-tracking branch 'origin/main' into feature/extend-holdings-endpoint-for-cash

pull/5650/head
KenTandrian 3 weeks ago
parent
commit
c2613f8c20
  1. 9
      CHANGELOG.md
  2. 5
      apps/api/src/app/app.module.ts
  3. 38
      apps/api/src/app/import/import.service.ts
  4. 66
      apps/api/src/app/portfolio/portfolio.controller.ts
  5. 8
      apps/api/src/app/portfolio/portfolio.service.ts
  6. 8
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  7. 6
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  8. 7
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  9. 2
      apps/client/src/locales/messages.de.xlf
  10. 2
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  11. 2
      libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
  12. 76
      package-lock.json
  13. 6
      package.json
  14. 53
      test/import/ok/penthouse-apartment.json

9
CHANGELOG.md

@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## 2.208.0 - 2025-10-11
### Added
@ -14,11 +14,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Changed the _As seen in_ section on the landing page to an animated carousel
- Refactored `transactionCount` to `activitiesCount` in the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
- Refactored various components to use self-closing tags
- Removed the deprecated endpoint `GET api/v1/portfolio/position/:dataSource/:symbol`
- Removed the deprecated endpoint `PUT api/v1/portfolio/position/:dataSource/:symbol/tags`
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `6.16.1` to `6.16.3`
### Fixed
- Fixed the server startup message to properly display IPv6 addresses
- Enabled IPv6 connectivity for _Redis_ in the job queue module by setting the address family
- Fixed an issue where importing custom asset profiles failed due to validation errors
## 2.207.0 - 2025-10-08

5
apps/api/src/app/app.module.ts

@ -71,9 +71,10 @@ import { UserModule } from './user/user.module';
BullModule.forRoot({
redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10),
family: 0,
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
password: process.env.REDIS_PASSWORD
password: process.env.REDIS_PASSWORD,
port: parseInt(process.env.REDIS_PORT ?? '6379', 10)
}
}),
CacheModule,

38
apps/api/src/app/import/import.service.ts

@ -373,6 +373,7 @@ export class ImportService {
const assetProfiles = await this.validateActivities({
activitiesDto,
assetProfilesWithMarketDataDto,
maxActivitiesToImport,
user
});
@ -698,10 +699,12 @@ export class ImportService {
private async validateActivities({
activitiesDto,
assetProfilesWithMarketDataDto,
maxActivitiesToImport,
user
}: {
activitiesDto: Partial<CreateOrderDto>[];
assetProfilesWithMarketDataDto: ImportDataDto['assetProfiles'];
maxActivitiesToImport: number;
user: UserWithSettings;
}) {
@ -749,6 +752,41 @@ export class ImportService {
)?.[symbol]
};
if (!assetProfile?.name) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(profile) => {
return (
profile.dataSource === dataSource && profile.symbol === symbol
);
}
);
if (assetProfileInImport) {
// Merge all fields of custom asset profiles into the validation object
Object.assign(assetProfile, {
assetClass: assetProfileInImport.assetClass,
assetSubClass: assetProfileInImport.assetSubClass,
comment: assetProfileInImport.comment,
countries: assetProfileInImport.countries,
currency: assetProfileInImport.currency,
cusip: assetProfileInImport.cusip,
dataSource: assetProfileInImport.dataSource,
figi: assetProfileInImport.figi,
figiComposite: assetProfileInImport.figiComposite,
figiShareClass: assetProfileInImport.figiShareClass,
holdings: assetProfileInImport.holdings,
isActive: assetProfileInImport.isActive,
isin: assetProfileInImport.isin,
name: assetProfileInImport.name,
scraperConfiguration: assetProfileInImport.scraperConfiguration,
sectors: assetProfileInImport.sectors,
symbol: assetProfileInImport.symbol,
symbolMapping: assetProfileInImport.symbolMapping,
url: assetProfileInImport.url
});
}
}
if (
(dataSource !== 'MANUAL' && type === 'BUY') ||
type === 'DIVIDEND' ||

66
apps/api/src/app/portfolio/portfolio.controller.ts

@ -610,36 +610,6 @@ export class PortfolioController {
return performanceInformation;
}
/**
* @deprecated
*/
@Get('position/:dataSource/:symbol')
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPosition(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<PortfolioHoldingResponse> {
const holding = await this.portfolioService.getHolding({
dataSource,
impersonationId,
symbol,
userId: this.request.user.id
});
if (!holding) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
return holding;
}
@Get('report')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getReport(
@ -699,40 +669,4 @@ export class PortfolioController {
userId: this.request.user.id
});
}
/**
* @deprecated
*/
@HasPermission(permissions.updateOrder)
@Put('position/:dataSource/:symbol/tags')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updatePositionTags(
@Body() data: UpdateHoldingTagsDto,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<void> {
const holding = await this.portfolioService.getHolding({
dataSource,
impersonationId,
symbol,
userId: this.request.user.id
});
if (!holding) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
await this.portfolioService.updateTags({
dataSource,
impersonationId,
symbol,
tags: data.tags,
userId: this.request.user.id
});
}
}

8
apps/api/src/app/portfolio/portfolio.service.ts

@ -796,6 +796,7 @@ export class PortfolioService {
if (activities.length === 0) {
return {
activities: [],
activitiesCount: 0,
averagePrice: undefined,
dataProviderInfo: undefined,
dividendInBaseCurrency: undefined,
@ -820,7 +821,6 @@ export class PortfolioService {
quantity: undefined,
SymbolProfile: undefined,
tags: [],
transactionCount: undefined,
value: undefined
};
}
@ -984,8 +984,8 @@ export class PortfolioService {
marketPriceMin,
SymbolProfile,
tags,
transactionCount,
activities: activitiesOfHolding,
activitiesCount: transactionCount,
averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
@ -1088,6 +1088,7 @@ export class PortfolioService {
marketPriceMin,
SymbolProfile,
activities: [],
activitiesCount: 0,
averagePrice: 0,
dataProviderInfo: undefined,
dividendInBaseCurrency: 0,
@ -1113,7 +1114,6 @@ export class PortfolioService {
},
quantity: 0,
tags: [],
transactionCount: undefined,
value: 0
};
}
@ -2152,7 +2152,7 @@ export class PortfolioService {
.plus(fees)
.toNumber(),
interest: interest.toNumber(),
liabilities: liabilities.toNumber(),
liabilitiesInBaseCurrency: liabilities.toNumber(),
totalInvestment: totalInvestment.toNumber(),
totalValueInBaseCurrency: netWorth
};

8
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -100,6 +100,7 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
templateUrl: 'holding-detail-dialog.html'
})
export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
public activitiesCount: number;
public activityForm: FormGroup;
public accounts: Account[];
public assetClass: string;
@ -151,8 +152,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
public SymbolProfile: EnhancedSymbolProfile;
public tags: Tag[];
public tagsAvailable: Tag[];
public totalItems: number;
public transactionCount: number;
public user: User;
public value: number;
@ -261,6 +260,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(
({
activitiesCount,
averagePrice,
dataProviderInfo,
dividendInBaseCurrency,
@ -279,9 +279,9 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
quantity,
SymbolProfile,
tags,
transactionCount,
value
}) => {
this.activitiesCount = activitiesCount;
this.averagePrice = averagePrice;
if (
@ -429,8 +429,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
this.activityForm.setValue({ tags: this.tags }, { emitEvent: false });
this.transactionCount = transactionCount;
this.totalItems = transactionCount;
this.value = value;
if (SymbolProfile?.assetClass) {

6
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -223,9 +223,9 @@
<gf-value
size="medium"
[locale]="data.locale"
[value]="transactionCount"
[value]="activitiesCount"
>
@if (transactionCount === 1) {
@if (activitiesCount === 1) {
<ng-container i18n>Activity</ng-container>
} @else {
<ng-container i18n>Activities</ng-container>
@ -363,7 +363,7 @@
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[sortDisabled]="true"
[totalItems]="totalItems"
[totalItems]="activitiesCount"
(activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)"
(export)="onExport()"

7
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -242,7 +242,10 @@
<div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate" i18n>Liabilities</div>
<div class="d-flex justify-content-end">
@if (summary?.liabilities || summary?.liabilities === 0) {
@if (
summary?.liabilitiesInBaseCurrency ||
summary?.liabilitiesInBaseCurrency === 0
) {
<span class="mr-1">-</span>
}
<gf-value
@ -250,7 +253,7 @@
[isCurrency]="true"
[locale]="locale"
[unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.liabilities"
[value]="isLoading ? undefined : summary?.liabilitiesInBaseCurrency"
/>
</div>
</div>

2
apps/client/src/locales/messages.de.xlf

@ -5409,7 +5409,7 @@
</trans-unit>
<trans-unit id="3302046820145091217" datatype="html">
<source>,</source>
<target state="translated">entnehmen,</target>
<target state="translated">&nbsp;entnehmen,</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">93</context>

2
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -21,7 +21,7 @@ export interface PortfolioSummary extends PortfolioPerformance {
grossPerformance: number;
grossPerformanceWithCurrencyEffect: number;
interest: number;
liabilities: number;
liabilitiesInBaseCurrency: number;
totalBuy: number;
totalSell: number;
totalValueInBaseCurrency?: number;

2
libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts

@ -10,6 +10,7 @@ import { Tag } from '@prisma/client';
export interface PortfolioHoldingResponse {
activities: Activity[];
activitiesCount: number;
averagePrice: number;
dataProviderInfo: DataProviderInfo;
dividendInBaseCurrency: number;
@ -34,6 +35,5 @@ export interface PortfolioHoldingResponse {
quantity: number;
SymbolProfile: EnhancedSymbolProfile;
tags: Tag[];
transactionCount: number;
value: number;
}

76
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "2.207.0",
"version": "2.208.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "2.207.0",
"version": "2.208.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -44,7 +44,7 @@
"@nestjs/schedule": "6.0.0",
"@nestjs/serve-static": "5.0.3",
"@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/client": "6.16.3",
"@prisma/client": "6.17.1",
"@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.9.0",
@ -149,7 +149,7 @@
"nx": "21.5.1",
"prettier": "3.6.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.16.3",
"prisma": "6.17.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "8.3.0",
@ -11960,9 +11960,9 @@
"license": "MIT"
},
"node_modules/@prisma/client": {
"version": "6.16.3",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.3.tgz",
"integrity": "sha512-JfNfAtXG+/lIopsvoZlZiH2k5yNx87mcTS4t9/S5oufM1nKdXYxOvpDC1XoTCFBa5cQh7uXnbMPsmZrwZY80xw==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz",
"integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
@ -11982,9 +11982,9 @@
}
},
"node_modules/@prisma/config": {
"version": "6.16.3",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz",
"integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz",
"integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
@ -11995,53 +11995,53 @@
}
},
"node_modules/@prisma/debug": {
"version": "6.16.3",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz",
"integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz",
"integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.16.3",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz",
"integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz",
"integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.16.3",
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"@prisma/fetch-engine": "6.16.3",
"@prisma/get-platform": "6.16.3"
"@prisma/debug": "6.17.1",
"@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"@prisma/fetch-engine": "6.17.1",
"@prisma/get-platform": "6.17.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz",
"integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==",
"version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz",
"integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.16.3",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz",
"integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz",
"integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.16.3",
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"@prisma/get-platform": "6.16.3"
"@prisma/debug": "6.17.1",
"@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"@prisma/get-platform": "6.17.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.16.3",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz",
"integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz",
"integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.16.3"
"@prisma/debug": "6.17.1"
}
},
"node_modules/@redis/client": {
@ -35747,15 +35747,15 @@
}
},
"node_modules/prisma": {
"version": "6.16.3",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz",
"integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz",
"integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.16.3",
"@prisma/engines": "6.16.3"
"@prisma/config": "6.17.1",
"@prisma/engines": "6.17.1"
},
"bin": {
"prisma": "build/index.js"

6
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.207.0",
"version": "2.208.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -90,7 +90,7 @@
"@nestjs/schedule": "6.0.0",
"@nestjs/serve-static": "5.0.3",
"@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/client": "6.16.3",
"@prisma/client": "6.17.1",
"@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.9.0",
@ -195,7 +195,7 @@
"nx": "21.5.1",
"prettier": "3.6.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.16.3",
"prisma": "6.17.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "8.3.0",

53
test/import/ok/penthouse-apartment.json

@ -0,0 +1,53 @@
{
"meta": {
"date": "2023-02-05T00:00:00.000Z",
"version": "dev"
},
"accounts": [],
"assetProfiles": [
{
"assetClass": null,
"assetSubClass": null,
"comment": null,
"countries": [],
"currency": "USD",
"cusip": null,
"dataSource": "MANUAL",
"figi": null,
"figiComposite": null,
"figiShareClass": null,
"holdings": [],
"isActive": true,
"isin": null,
"marketData": [],
"name": "Penthouse Apartment",
"scraperConfiguration": null,
"sectors": [],
"symbol": "7e91b7d4-1430-4212-8380-289a06c9bbc1",
"symbolMapping": {},
"url": null
}
],
"platforms": [],
"tags": [],
"activities": [
{
"accountId": null,
"comment": null,
"currency": "USD",
"dataSource": "MANUAL",
"date": "2022-01-01T00:00:00.000Z",
"fee": 0,
"quantity": 1,
"symbol": "7e91b7d4-1430-4212-8380-289a06c9bbc1",
"tags": [],
"type": "BUY",
"unitPrice": 500000,
}
],
"user": {
"settings": {
"currency": "USD"
}
}
}
Loading…
Cancel
Save