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 { 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 { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { DataSource, Prisma } from '@prisma/client';
@Injectable() @Injectable()
export class DevSeedService { export class DevSeedService {
private readonly logger = new Logger(DevSeedService.name); 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, * Wipe ALL seeded data: accounts, activities, symbol profiles, market data,
@ -247,11 +251,15 @@ export class DevSeedService {
const acctCoinbase = accountRecords[4]; const acctCoinbase = accountRecords[4];
const acctIBKR = accountRecords[5]; 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: { const symbolDefs: {
assetClass: string; assetClass: string;
assetSubClass: string; assetSubClass: string;
currency: string; currency: string;
dataSource: DataSource;
key?: string;
name: string; name: string;
symbol: string; symbol: string;
}[] = [ }[] = [
@ -260,6 +268,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Apple Inc.', name: 'Apple Inc.',
symbol: 'AAPL' symbol: 'AAPL'
}, },
@ -267,6 +276,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Microsoft Corporation', name: 'Microsoft Corporation',
symbol: 'MSFT' symbol: 'MSFT'
}, },
@ -274,6 +284,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Amazon.com Inc.', name: 'Amazon.com Inc.',
symbol: 'AMZN' symbol: 'AMZN'
}, },
@ -281,6 +292,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'NVIDIA Corporation', name: 'NVIDIA Corporation',
symbol: 'NVDA' symbol: 'NVDA'
}, },
@ -288,6 +300,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Alphabet Inc.', name: 'Alphabet Inc.',
symbol: 'GOOGL' symbol: 'GOOGL'
}, },
@ -295,6 +308,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Tesla Inc.', name: 'Tesla Inc.',
symbol: 'TSLA' symbol: 'TSLA'
}, },
@ -302,6 +316,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'JPMorgan Chase & Co.', name: 'JPMorgan Chase & Co.',
symbol: 'JPM' symbol: 'JPM'
}, },
@ -309,6 +324,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Johnson & Johnson', name: 'Johnson & Johnson',
symbol: 'JNJ' symbol: 'JNJ'
}, },
@ -317,6 +333,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Vanguard S&P 500 ETF', name: 'Vanguard S&P 500 ETF',
symbol: 'VOO' symbol: 'VOO'
}, },
@ -324,6 +341,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Vanguard Total Stock Market ETF', name: 'Vanguard Total Stock Market ETF',
symbol: 'VTI' symbol: 'VTI'
}, },
@ -331,6 +349,7 @@ export class DevSeedService {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Vanguard Total International ETF', name: 'Vanguard Total International ETF',
symbol: 'VXUS' symbol: 'VXUS'
}, },
@ -338,6 +357,7 @@ export class DevSeedService {
assetClass: 'FIXED_INCOME', assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND', assetSubClass: 'BOND',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'iShares Core US Aggregate Bond ETF', name: 'iShares Core US Aggregate Bond ETF',
symbol: 'AGG' symbol: 'AGG'
}, },
@ -345,29 +365,36 @@ export class DevSeedService {
assetClass: 'REAL_ESTATE', assetClass: 'REAL_ESTATE',
assetSubClass: 'ETF', assetSubClass: 'ETF',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Vanguard Real Estate ETF', name: 'Vanguard Real Estate ETF',
symbol: 'VNQ' 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', assetClass: 'ALTERNATIVE_INVESTMENT',
assetSubClass: 'CRYPTOCURRENCY', assetSubClass: 'CRYPTOCURRENCY',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
key: 'BTC',
name: 'Bitcoin', name: 'Bitcoin',
symbol: 'BTC' symbol: 'BTCUSD'
}, },
{ {
assetClass: 'ALTERNATIVE_INVESTMENT', assetClass: 'ALTERNATIVE_INVESTMENT',
assetSubClass: 'CRYPTOCURRENCY', assetSubClass: 'CRYPTOCURRENCY',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
key: 'ETH',
name: 'Ethereum', name: 'Ethereum',
symbol: 'ETH' symbol: 'ETHUSD'
}, },
// Commodities // Commodities
{ {
assetClass: 'COMMODITY', assetClass: 'COMMODITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'SPDR Gold Shares', name: 'SPDR Gold Shares',
symbol: 'GLD' symbol: 'GLD'
} }
@ -381,7 +408,7 @@ export class DevSeedService {
assetClass: def.assetClass as any, assetClass: def.assetClass as any,
assetSubClass: def.assetSubClass as any, assetSubClass: def.assetSubClass as any,
currency: def.currency, currency: def.currency,
dataSource: 'MANUAL', dataSource: def.dataSource,
name: def.name, name: def.name,
symbol: def.symbol symbol: def.symbol
}, },
@ -391,132 +418,24 @@ export class DevSeedService {
name: def.name name: def.name
}, },
where: { 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++; counts.symbolProfiles++;
} }
// ── A5. Market Data — 3+ years of monthly prices ──────────────── // ── A5. Market Data — fetched from live providers ────────────────
// Realistic price trajectories (monthly, Jan 2023 → Dec 2025) // Historical prices will be gathered asynchronously by the data
const priceHistories: Record<string, number[]> = { // gathering queue after seed completes (see gatherMax() at the end).
AAPL: [ // No hardcoded prices needed — Yahoo Finance (stocks/ETFs) and
130, 135, 140, 148, 156, 168, 172, 178, 185, 175, 182, 190, 192, 195, // Yahoo Finance provides real historical data for all symbols.
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;
}
// ── A6. Activities / Orders — 3 years of realistic trading ────── // ── A6. Activities / Orders — 3 years of realistic trading ──────
const activities = this.buildActivities({ const activities = this.buildActivities({
@ -980,6 +899,15 @@ export class DevSeedService {
this.logger.log(`Dummy data populated: ${JSON.stringify(counts)}`); 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 }; return { created: counts };
} }

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

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

Loading…
Cancel
Save