fix bruno collection
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { PortfolioAdvisor } from '../server/domains/portfolio/PortfolioAdvisor.js';
|
||||
import { SIGNAL } from '../server/domains/shared/config/constants.js';
|
||||
import type {
|
||||
PortfolioHolding,
|
||||
AdviceRow,
|
||||
} from '../server/domains/shared/types/portfolio.model.js';
|
||||
import type { ScreenerResult } from '../server/domains/shared/types/asset.model.js';
|
||||
|
||||
class MockYahooClient {
|
||||
async fetchSummary(ticker: string) {
|
||||
const prices: Record<string, number> = {
|
||||
AAPL: 189.5,
|
||||
MSFT: 425.3,
|
||||
'BRK-B': 385.0,
|
||||
};
|
||||
|
||||
return {
|
||||
price: {
|
||||
regularMarketPrice: prices[ticker] || 100,
|
||||
marketCap: 1e12,
|
||||
},
|
||||
summaryDetail: {
|
||||
fiftyTwoWeekHigh: (prices[ticker] || 100) * 1.1,
|
||||
fiftyTwoWeekLow: (prices[ticker] || 100) * 0.9,
|
||||
},
|
||||
quoteType: { quoteType: 'EQUITY' },
|
||||
defaultKeyStatistics: { trailingPE: 20 },
|
||||
financialData: {
|
||||
returnOnEquity: 0.2,
|
||||
operatingMargins: 0.15,
|
||||
grossMargins: 0.4,
|
||||
freeCashflow: 50e9,
|
||||
totalRevenue: 200e9,
|
||||
debtToEquity: 50,
|
||||
currentRatio: 1.2,
|
||||
},
|
||||
incomeStatementHistoryQuarterly: {
|
||||
incomeStatementHistory: [{ commonStockSharesOutstanding: 2.6e9, netIncome: 25e9 }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
normalise(ticker: string): string {
|
||||
return ticker.replace(/\./g, '-');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: build a minimal ScreenerResult with one STOCK result
|
||||
function makeScreenerResult(ticker: string, signal: string, price: number): ScreenerResult {
|
||||
return {
|
||||
STOCK: [
|
||||
{
|
||||
signal,
|
||||
fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
asset: {
|
||||
ticker,
|
||||
currentPrice: price,
|
||||
type: 'STOCK',
|
||||
getDisplayMetrics: () => ({}),
|
||||
} as any,
|
||||
displayMetrics: {},
|
||||
} as any,
|
||||
],
|
||||
ETF: [],
|
||||
BOND: [],
|
||||
ERROR: [],
|
||||
marketContext: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
test('PortfolioAdvisor', async (t) => {
|
||||
await t.test('analyzes portfolio with single holding', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5);
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
assert.equal(advice.length, 1);
|
||||
});
|
||||
|
||||
await t.test('calculates position gain/loss correctly', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5);
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
const row = advice[0] as AdviceRow;
|
||||
// gain = 10 * (189.5 - 150) = $395
|
||||
assert.ok(row.gainLossPct !== null);
|
||||
});
|
||||
|
||||
await t.test('handles empty portfolio', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const screened: ScreenerResult = {
|
||||
STOCK: [],
|
||||
ETF: [],
|
||||
BOND: [],
|
||||
ERROR: [],
|
||||
marketContext: null as any,
|
||||
};
|
||||
const advice = await advisor.advise([], screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
assert.equal(advice.length, 0);
|
||||
});
|
||||
|
||||
await t.test('normalizes BRK.B to BRK-B', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'BRK.B', shares: 5, costBasis: 350, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened = makeScreenerResult('BRK-B', SIGNAL.NEUTRAL, 385.0);
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
assert.equal(advice.length, 1);
|
||||
});
|
||||
|
||||
await t.test('analyzes multiple holdings', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
{ ticker: 'MSFT', shares: 5, costBasis: 400, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened: ScreenerResult = {
|
||||
STOCK: [
|
||||
{
|
||||
signal: SIGNAL.STRONG_BUY,
|
||||
fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
asset: {
|
||||
ticker: 'AAPL',
|
||||
currentPrice: 189.5,
|
||||
type: 'STOCK',
|
||||
getDisplayMetrics: () => ({}),
|
||||
} as any,
|
||||
displayMetrics: {},
|
||||
} as any,
|
||||
{
|
||||
signal: SIGNAL.BUY,
|
||||
fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
asset: {
|
||||
ticker: 'MSFT',
|
||||
currentPrice: 425.3,
|
||||
type: 'STOCK',
|
||||
getDisplayMetrics: () => ({}),
|
||||
} as any,
|
||||
displayMetrics: {},
|
||||
} as any,
|
||||
],
|
||||
ETF: [],
|
||||
BOND: [],
|
||||
ERROR: [],
|
||||
marketContext: null as any,
|
||||
};
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
assert.equal(advice.length, 2);
|
||||
});
|
||||
|
||||
await t.test('maps signal to advice recommendation', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
];
|
||||
|
||||
for (const signal of [SIGNAL.STRONG_BUY, SIGNAL.NEUTRAL, SIGNAL.AVOID]) {
|
||||
const screened = makeScreenerResult('AAPL', signal, 189.5);
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('handles fractional shares', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'AAPL', shares: 10.5, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5);
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
assert.equal(advice.length, 1);
|
||||
});
|
||||
|
||||
await t.test('returns advice rows with ticker information', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5);
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
const row = advice[0] as AdviceRow;
|
||||
assert.ok(row.ticker);
|
||||
});
|
||||
|
||||
await t.test('handles missing current price gracefully', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'UNKNOWN', shares: 10, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened: ScreenerResult = {
|
||||
STOCK: [],
|
||||
ETF: [],
|
||||
BOND: [],
|
||||
ERROR: [],
|
||||
marketContext: null as any,
|
||||
};
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
assert.equal(advice.length, 1);
|
||||
assert.equal(advice[0].currentPrice, null);
|
||||
});
|
||||
|
||||
await t.test('calculates total portfolio value', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
{ ticker: 'MSFT', shares: 5, costBasis: 400, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened: ScreenerResult = {
|
||||
STOCK: [
|
||||
{
|
||||
signal: SIGNAL.STRONG_BUY,
|
||||
fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
asset: {
|
||||
ticker: 'AAPL',
|
||||
currentPrice: 189.5,
|
||||
type: 'STOCK',
|
||||
getDisplayMetrics: () => ({}),
|
||||
} as any,
|
||||
displayMetrics: {},
|
||||
} as any,
|
||||
{
|
||||
signal: SIGNAL.BUY,
|
||||
fundamental: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
inflated: { label: 'pass', scoreSummary: '', audit: { passedGates: true } },
|
||||
asset: {
|
||||
ticker: 'MSFT',
|
||||
currentPrice: 425.3,
|
||||
type: 'STOCK',
|
||||
getDisplayMetrics: () => ({}),
|
||||
} as any,
|
||||
displayMetrics: {},
|
||||
} as any,
|
||||
],
|
||||
ETF: [],
|
||||
BOND: [],
|
||||
ERROR: [],
|
||||
marketContext: null as any,
|
||||
};
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
assert.equal(advice.length, 2);
|
||||
// AAPL: 10 * 189.5 = 1895, MSFT: 5 * 425.3 = 2126.5
|
||||
const totalValue = advice.reduce((sum, r) => sum + parseFloat(r.marketValue ?? '0'), 0);
|
||||
assert.ok(totalValue > 0);
|
||||
});
|
||||
|
||||
await t.test('signals match in advice rows', async () => {
|
||||
const advisor = new PortfolioAdvisor(new MockYahooClient() as any);
|
||||
const holdings: PortfolioHolding[] = [
|
||||
{ ticker: 'AAPL', shares: 10, costBasis: 150, source: 'manual', type: 'stock' },
|
||||
];
|
||||
const screened = makeScreenerResult('AAPL', SIGNAL.STRONG_BUY, 189.5);
|
||||
const advice = await advisor.advise(holdings, screened);
|
||||
assert.ok(Array.isArray(advice));
|
||||
const row = advice[0] as AdviceRow;
|
||||
assert.equal(row.signal, SIGNAL.STRONG_BUY);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user