Browse Source

fix: resolve dollar-one current-day price display

pull/6701/head
Robert Patch 2 months ago
parent
commit
1e5aab5bf1
  1. 182
      apps/api/src/app/admin/dev-seed.service.ts
  2. 9
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 24
      apps/api/src/app/portfolio/current-rate.service.ts

182
apps/api/src/app/admin/dev-seed.service.ts

@ -1,13 +1,17 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { DataSource, Prisma } from '@prisma/client';
@Injectable()
export class DevSeedService {
private readonly logger = new Logger(DevSeedService.name);
public constructor(private readonly prismaService: PrismaService) {}
public constructor(
private readonly dataGatheringService: DataGatheringService,
private readonly prismaService: PrismaService
) {}
/**
* Wipe ALL seeded data: accounts, activities, symbol profiles, market data,
@ -247,11 +251,15 @@ export class DevSeedService {
const acctCoinbase = accountRecords[4];
const acctIBKR = accountRecords[5];
// ── A4. Symbol Profiles (MANUAL data source) ────────────────────
// ── A4. Symbol Profiles (live data sources) ─────────────────────
// All symbols use YAHOO (full historical data, including crypto via BTCUSD/ETHUSD).
// The optional `key` field is an alias used in buildActivities().
const symbolDefs: {
assetClass: string;
assetSubClass: string;
currency: string;
dataSource: DataSource;
key?: string;
name: string;
symbol: string;
}[] = [
@ -260,6 +268,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Apple Inc.',
symbol: 'AAPL'
},
@ -267,6 +276,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Microsoft Corporation',
symbol: 'MSFT'
},
@ -274,6 +284,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Amazon.com Inc.',
symbol: 'AMZN'
},
@ -281,6 +292,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'NVIDIA Corporation',
symbol: 'NVDA'
},
@ -288,6 +300,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Alphabet Inc.',
symbol: 'GOOGL'
},
@ -295,6 +308,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Tesla Inc.',
symbol: 'TSLA'
},
@ -302,6 +316,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'JPMorgan Chase & Co.',
symbol: 'JPM'
},
@ -309,6 +324,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Johnson & Johnson',
symbol: 'JNJ'
},
@ -317,6 +333,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'ETF',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Vanguard S&P 500 ETF',
symbol: 'VOO'
},
@ -324,6 +341,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'ETF',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Vanguard Total Stock Market ETF',
symbol: 'VTI'
},
@ -331,6 +349,7 @@ export class DevSeedService {
assetClass: 'EQUITY',
assetSubClass: 'ETF',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Vanguard Total International ETF',
symbol: 'VXUS'
},
@ -338,6 +357,7 @@ export class DevSeedService {
assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'iShares Core US Aggregate Bond ETF',
symbol: 'AGG'
},
@ -345,29 +365,36 @@ export class DevSeedService {
assetClass: 'REAL_ESTATE',
assetSubClass: 'ETF',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Vanguard Real Estate ETF',
symbol: 'VNQ'
},
// Crypto
// Crypto — internal symbol is BTCUSD/ETHUSD; the Yahoo Finance data
// enhancer automatically converts to BTC-USD/ETH-USD for API calls.
{
assetClass: 'ALTERNATIVE_INVESTMENT',
assetSubClass: 'CRYPTOCURRENCY',
currency: 'USD',
dataSource: DataSource.YAHOO,
key: 'BTC',
name: 'Bitcoin',
symbol: 'BTC'
symbol: 'BTCUSD'
},
{
assetClass: 'ALTERNATIVE_INVESTMENT',
assetSubClass: 'CRYPTOCURRENCY',
currency: 'USD',
dataSource: DataSource.YAHOO,
key: 'ETH',
name: 'Ethereum',
symbol: 'ETH'
symbol: 'ETHUSD'
},
// Commodities
{
assetClass: 'COMMODITY',
assetSubClass: 'ETF',
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'SPDR Gold Shares',
symbol: 'GLD'
}
@ -381,7 +408,7 @@ export class DevSeedService {
assetClass: def.assetClass as any,
assetSubClass: def.assetSubClass as any,
currency: def.currency,
dataSource: 'MANUAL',
dataSource: def.dataSource,
name: def.name,
symbol: def.symbol
},
@ -391,132 +418,24 @@ export class DevSeedService {
name: def.name
},
where: {
dataSource_symbol: { dataSource: 'MANUAL', symbol: def.symbol }
dataSource_symbol: {
dataSource: def.dataSource,
symbol: def.symbol
}
}
});
symbolProfileMap[def.symbol] = sp.id;
// Use `key` alias (e.g. 'BTC') when present so buildActivities()
// can still reference symbols by their common ticker.
symbolProfileMap[def.key ?? def.symbol] = sp.id;
counts.symbolProfiles++;
}
// ── A5. Market Data — 3+ years of monthly prices ────────────────
// Realistic price trajectories (monthly, Jan 2023 → Dec 2025)
const priceHistories: Record<string, number[]> = {
AAPL: [
130, 135, 140, 148, 156, 168, 172, 178, 185, 175, 182, 190, 192, 195,
198, 205, 210, 215, 220, 225, 218, 222, 228, 235, 232, 238, 245, 250,
248, 255, 260, 258, 262, 270, 275, 280
],
MSFT: [
240, 248, 255, 262, 275, 290, 305, 318, 325, 330, 340, 360, 365, 370,
378, 392, 400, 410, 420, 430, 425, 435, 445, 455, 450, 458, 465, 475,
480, 488, 495, 500, 505, 512, 520, 530
],
AMZN: [
85, 90, 95, 102, 110, 118, 125, 130, 128, 135, 140, 148, 152, 155,
160, 168, 175, 180, 186, 188, 182, 190, 195, 200, 198, 205, 210, 215,
218, 225, 230, 228, 235, 240, 245, 250
],
NVDA: [
142, 155, 175, 210, 250, 295, 350, 410, 445, 460, 470, 495, 510, 540,
580, 650, 720, 800, 850, 900, 880, 920, 950, 980, 960, 990, 1020,
1050, 1080, 1120, 1100, 1150, 1180, 1200, 1230, 1250
],
GOOGL: [
88, 92, 96, 102, 110, 118, 120, 125, 130, 128, 132, 138, 142, 145,
150, 155, 160, 165, 170, 175, 172, 178, 182, 188, 185, 190, 195, 200,
205, 210, 215, 212, 218, 222, 228, 235
],
TSLA: [
120, 135, 162, 180, 195, 205, 225, 245, 255, 240, 215, 250, 248, 260,
275, 290, 280, 265, 250, 240, 255, 270, 285, 300, 310, 320, 305, 295,
310, 325, 340, 350, 345, 360, 375, 390
],
JPM: [
135, 138, 128, 132, 136, 142, 148, 152, 155, 150, 148, 158, 162, 168,
175, 180, 185, 190, 195, 200, 198, 205, 210, 218, 215, 220, 225, 230,
228, 235, 240, 245, 242, 248, 255, 260
],
JNJ: [
172, 168, 162, 158, 155, 160, 162, 165, 158, 155, 152, 158, 160, 162,
165, 168, 170, 172, 168, 165, 162, 165, 168, 170, 168, 170, 172, 175,
178, 180, 182, 180, 178, 182, 185, 188
],
VOO: [
365, 370, 378, 390, 398, 408, 420, 430, 435, 425, 430, 445, 450, 458,
465, 475, 480, 490, 498, 505, 495, 502, 510, 520, 515, 522, 530, 538,
542, 550, 555, 560, 558, 565, 572, 580
],
VTI: [
195, 198, 202, 208, 215, 222, 228, 234, 238, 230, 235, 242, 245, 250,
255, 260, 265, 270, 275, 280, 275, 278, 282, 290, 288, 292, 298, 302,
305, 310, 315, 318, 315, 320, 325, 330
],
VXUS: [
52, 53, 54, 55, 56, 55, 54, 53, 52, 51, 52, 53, 54, 55, 56, 57, 58,
57, 56, 55, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 63, 62, 63,
64, 65
],
AGG: [
98, 97, 96, 97, 96, 95, 94, 93, 92, 91, 92, 93, 94, 94, 95, 95, 96,
96, 97, 97, 98, 98, 99, 99, 100, 100, 101, 101, 102, 102, 103, 103,
104, 104, 105, 105
],
VNQ: [
82, 80, 78, 77, 75, 78, 80, 82, 80, 78, 76, 80, 82, 84, 86, 88, 90,
92, 90, 88, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 105, 103,
105, 107, 110
],
BTC: [
16500, 19500, 23000, 28000, 27500, 30000, 31000, 29500, 26000, 27500,
34000, 42000, 44000, 48000, 52000, 57000, 63000, 68000, 65000, 62000,
58000, 60000, 72000, 85000, 82000, 88000, 92000, 95000, 98000, 102000,
105000, 100000, 97000, 103000, 108000, 112000
],
ETH: [
1200, 1400, 1600, 1850, 1800, 1900, 1950, 1850, 1650, 1700, 2000,
2300, 2400, 2600, 2800, 3100, 3400, 3600, 3500, 3300, 3100, 3200,
3500, 3800, 3700, 3900, 4100, 4300, 4500, 4700, 4600, 4400, 4200,
4500, 4800, 5000
],
GLD: [
170, 172, 178, 182, 185, 182, 180, 178, 175, 180, 185, 190, 192, 195,
198, 200, 205, 210, 215, 218, 220, 225, 230, 235, 238, 242, 248, 252,
258, 262, 265, 268, 272, 278, 282, 288
]
};
for (const [symbol, prices] of Object.entries(priceHistories)) {
const dataPoints: {
dataSource: 'MANUAL';
date: Date;
marketPrice: number;
symbol: string;
}[] = [];
for (let i = 0; i < prices.length; i++) {
const year = 2023 + Math.floor(i / 12);
const month = (i % 12) + 1;
// Use last business day of the month
const date = new Date(
`${year}-${String(month).padStart(2, '0')}-${month === 2 ? '28' : '30'}T00:00:00.000Z`
);
dataPoints.push({
dataSource: 'MANUAL',
date,
marketPrice: prices[i],
symbol
});
}
await this.prismaService.marketData.createMany({
data: dataPoints as any,
skipDuplicates: true
});
counts.marketData += dataPoints.length;
}
// ── A5. Market Data — fetched from live providers ────────────────
// Historical prices will be gathered asynchronously by the data
// gathering queue after seed completes (see gatherMax() at the end).
// No hardcoded prices needed — Yahoo Finance (stocks/ETFs) and
// Yahoo Finance provides real historical data for all symbols.
// ── A6. Activities / Orders — 3 years of realistic trading ──────
const activities = this.buildActivities({
@ -980,6 +899,15 @@ export class DevSeedService {
this.logger.log(`Dummy data populated: ${JSON.stringify(counts)}`);
// ── Gather real historical market data from live providers ───────
// This enqueues background jobs for Yahoo Finance (stocks/ETFs) and
// (BTCUSD, ETHUSD, stocks, ETFs). Prices arrive asynchronously — the UI will
// populate within ~30–60 seconds as the queue drains.
this.logger.log(
'Enqueuing market-data gathering jobs (Yahoo Finance)…'
);
await this.dataGatheringService.gatherMax();
return { created: counts };
}

9
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -431,8 +431,13 @@ export abstract class PortfolioCalculator {
investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
marketPrice:
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? 1,
marketPriceInBaseCurrency: marketPriceInBaseCurrency?.toNumber() ?? 1,
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ??
item.averagePrice?.toNumber() ??
0,
marketPriceInBaseCurrency:
marketPriceInBaseCurrency?.toNumber() ??
item.averagePrice?.toNumber() ??
0,
netPerformance: !hasErrors ? (netPerformance ?? null) : null,
netPerformancePercentage: !hasErrors
? (netPerformancePercentage ?? null)

24
apps/api/src/app/portfolio/current-rate.service.ts

@ -2,7 +2,6 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.ser
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { resetHours } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
DataProviderInfo,
@ -10,9 +9,9 @@ import {
} from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { isBefore, isToday } from 'date-fns';
import { isBefore, isToday, startOfDay } from 'date-fns';
import { isEmpty, uniqBy } from 'lodash';
import { GetValueObject } from './interfaces/get-value-object.interface';
@ -44,7 +43,7 @@ export class CurrentRateService {
(!dateQuery.in || this.containsToday(dateQuery.in));
const quoteErrors: ResponseError['errors'] = [];
const today = resetHours(new Date());
const today = startOfDay(new Date());
const values: GetValueObject[] = [];
if (includesToday) {
@ -147,7 +146,11 @@ export class CurrentRateService {
const [latestValue] = response.values
.filter((currentValue) => {
return currentValue.symbol === symbol && currentValue.marketPrice;
return (
currentValue.symbol === symbol &&
currentValue.marketPrice != null &&
currentValue.marketPrice > 0
);
})
.sort((a, b) => {
if (a.date < b.date) {
@ -161,8 +164,15 @@ export class CurrentRateService {
return 0;
});
value.marketPrice = latestValue.marketPrice;
} catch {}
if (latestValue?.marketPrice) {
value.marketPrice = latestValue.marketPrice;
}
} catch (error) {
Logger.warn(
`Failed to recover quote for ${symbol}: ${error?.message}`,
'CurrentRateService'
);
}
}
}

Loading…
Cancel
Save