phase-1: optimize code
This commit is contained in:
@@ -78,6 +78,63 @@ test('FCF yield is computed when data available', () => {
|
||||
assert(result.fcfYield > 0);
|
||||
});
|
||||
|
||||
test('peRatio prefers trailingPE over forwardPE', () => {
|
||||
// trailingPE=30 in summaryDetail, forwardPE=28 in defaultKeyStatistics
|
||||
const result = mapToStandardFormat('AAPL', base);
|
||||
assert.equal(result.peRatio, 30); // trailing should win
|
||||
});
|
||||
|
||||
test('negative FCF yield is preserved, not nulled', () => {
|
||||
const negativeFcf = {
|
||||
...base,
|
||||
financialData: { ...base.financialData, freeCashflow: -2e9 },
|
||||
};
|
||||
const result = mapToStandardFormat('AAPL', negativeFcf);
|
||||
assert.notEqual(result.fcfYield, null);
|
||||
assert(result.fcfYield < 0, 'negative FCF should produce negative yield, not null');
|
||||
});
|
||||
|
||||
test('ETF maps volume from summaryDetail', () => {
|
||||
const etfSummary = {
|
||||
...base,
|
||||
price: { ...base.price, quoteType: 'ETF' },
|
||||
assetProfile: { category: 'Large Blend' },
|
||||
summaryDetail: {
|
||||
...base.summaryDetail,
|
||||
averageVolume: 5000000,
|
||||
expenseRatio: 0.0003,
|
||||
trailingAnnualDividendYield: 0.013,
|
||||
},
|
||||
defaultKeyStatistics: { fiveYearAverageReturn: 0.12 },
|
||||
};
|
||||
const result = mapToStandardFormat('VOO', etfSummary);
|
||||
assert.equal(result.volume, 5000000);
|
||||
});
|
||||
|
||||
test('bond duration inferred from category — intermediate maps to 5y', () => {
|
||||
const bondSummary = {
|
||||
...base,
|
||||
price: { ...base.price, quoteType: 'ETF' },
|
||||
assetProfile: { category: 'Intermediate-Term Bond' },
|
||||
summaryDetail: { yield: 0.045 },
|
||||
defaultKeyStatistics: {},
|
||||
};
|
||||
const result = mapToStandardFormat('BND', bondSummary);
|
||||
assert.equal(result.duration, 5);
|
||||
});
|
||||
|
||||
test('bond duration inferred from category — short-term maps to 2y', () => {
|
||||
const bondSummary = {
|
||||
...base,
|
||||
price: { ...base.price, quoteType: 'ETF' },
|
||||
assetProfile: { category: 'Short-Term Bond' },
|
||||
summaryDetail: { yield: 0.05 },
|
||||
defaultKeyStatistics: {},
|
||||
};
|
||||
const result = mapToStandardFormat('SHY', bondSummary);
|
||||
assert.equal(result.duration, 2);
|
||||
});
|
||||
|
||||
test('metrics are null (not 0) when data missing', () => {
|
||||
const sparse = {
|
||||
price: { quoteType: 'EQUITY', regularMarketPrice: 100 },
|
||||
|
||||
@@ -29,3 +29,26 @@ test('audit breakdown includes cost, yield, vol keys', () => {
|
||||
assert(result.audit.breakdown.yield != null);
|
||||
assert(result.audit.breakdown.vol != null);
|
||||
});
|
||||
|
||||
test('penalises ETF with volume below liquidity floor', () => {
|
||||
const result = EtfScorer.score({ expenseRatio: 0.03, yield: 2.0, volume: 100000 }, rules);
|
||||
assert(result.audit.breakdown.vol < 0, 'low-volume ETF should receive negative vol score');
|
||||
});
|
||||
|
||||
test('scores 5Y return when threshold configured', () => {
|
||||
const rulesWithReturn = {
|
||||
...rules,
|
||||
weights: { ...rules.weights, fiveYearReturn: 2 },
|
||||
thresholds: { ...rules.thresholds, minFiveYearReturn: 8.0 },
|
||||
};
|
||||
const good = EtfScorer.score(
|
||||
{ expenseRatio: 0.03, yield: 2.0, volume: 1000000, fiveYearReturn: 10 },
|
||||
rulesWithReturn,
|
||||
);
|
||||
const poor = EtfScorer.score(
|
||||
{ expenseRatio: 0.03, yield: 2.0, volume: 1000000, fiveYearReturn: 5 },
|
||||
rulesWithReturn,
|
||||
);
|
||||
assert(good.audit.breakdown.fiveYearReturn > 0, 'strong 5Y return should score positively');
|
||||
assert(poor.audit.breakdown.fiveYearReturn < 0, 'weak 5Y return should score negatively');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// Test the markdown fence stripping logic in isolation —
|
||||
// we don't instantiate LLMAnalyst (requires Anthropic SDK + API key).
|
||||
// The regex is: raw.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim()
|
||||
|
||||
function stripFences(raw) {
|
||||
return raw
|
||||
.replace(/^```(?:json)?\s*/i, '')
|
||||
.replace(/```\s*$/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
const VALID_JSON =
|
||||
'{"summary":"test","sentiment":"BULLISH","affectedIndustries":[],"relatedTickers":[]}';
|
||||
|
||||
test('stripFences: passes clean JSON through unchanged', () => {
|
||||
assert.equal(stripFences(VALID_JSON), VALID_JSON);
|
||||
});
|
||||
|
||||
test('stripFences: strips ```json ... ``` fences', () => {
|
||||
const wrapped = '```json\n' + VALID_JSON + '\n```';
|
||||
assert.equal(stripFences(wrapped), VALID_JSON);
|
||||
});
|
||||
|
||||
test('stripFences: strips ``` ... ``` fences (no language tag)', () => {
|
||||
const wrapped = '```\n' + VALID_JSON + '\n```';
|
||||
assert.equal(stripFences(wrapped), VALID_JSON);
|
||||
});
|
||||
|
||||
test('stripFences: result is valid parseable JSON', () => {
|
||||
const wrapped = '```json\n' + VALID_JSON + '\n```';
|
||||
const parsed = JSON.parse(stripFences(wrapped));
|
||||
assert.equal(parsed.sentiment, 'BULLISH');
|
||||
assert.equal(parsed.summary, 'test');
|
||||
});
|
||||
|
||||
test('stripFences: handles no trailing newline before closing fence', () => {
|
||||
const wrapped = '```json\n' + VALID_JSON + '```';
|
||||
assert.equal(stripFences(wrapped), VALID_JSON);
|
||||
});
|
||||
|
||||
test('stripFences: case-insensitive fence tag', () => {
|
||||
const wrapped = '```JSON\n' + VALID_JSON + '\n```';
|
||||
assert.equal(stripFences(wrapped), VALID_JSON);
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import { MarketRegime } from '../src/market/MarketRegime.js';
|
||||
import { SECTOR, ASSET_TYPE } from '../src/config/constants.js';
|
||||
|
||||
const regime = (benchmarks) => new MarketRegime({ benchmarks });
|
||||
const regime = (benchmarks, extra = {}) => new MarketRegime({ benchmarks, ...extra });
|
||||
|
||||
test('stock inflated P/E = marketPE × 1.5', () => {
|
||||
const { gates } = regime({ marketPE: 24 }).getInflatedOverrides(ASSET_TYPE.STOCK, SECTOR.GENERAL);
|
||||
@@ -18,22 +18,46 @@ test('tech inflated P/E = techPE × 1.3', () => {
|
||||
assert.equal(gates.maxPERatio, Math.round(40 * 1.3)); // 52
|
||||
});
|
||||
|
||||
test('REIT inflated minYield = reitYield × 0.85', () => {
|
||||
const { thresholds } = regime({ reitYield: 4.0 }).getInflatedOverrides(
|
||||
test('REIT inflated minYield = reitYield × 0.85 in NORMAL rate regime', () => {
|
||||
const { thresholds } = regime({ reitYield: 4.0 }, { rateRegime: 'NORMAL' }).getInflatedOverrides(
|
||||
ASSET_TYPE.STOCK,
|
||||
SECTOR.REIT,
|
||||
);
|
||||
assert.equal(thresholds.minYield, +(4.0 * 0.85).toFixed(2)); // 3.40
|
||||
});
|
||||
|
||||
test('bond inflated minSpread = igSpread × 0.80', () => {
|
||||
const { thresholds } = regime({ igSpread: 1.5 }).getInflatedOverrides(
|
||||
test('REIT inflated minYield = reitYield × 0.95 in HIGH rate regime', () => {
|
||||
const { thresholds } = regime({ reitYield: 4.0 }, { rateRegime: 'HIGH' }).getInflatedOverrides(
|
||||
ASSET_TYPE.STOCK,
|
||||
SECTOR.REIT,
|
||||
);
|
||||
assert.equal(thresholds.minYield, +(4.0 * 0.95).toFixed(2)); // 3.80
|
||||
});
|
||||
|
||||
test('bond inflated minSpread = igSpread × 0.80 in NORMAL rate regime', () => {
|
||||
const { thresholds } = regime({ igSpread: 1.5 }, { rateRegime: 'NORMAL' }).getInflatedOverrides(
|
||||
ASSET_TYPE.BOND,
|
||||
SECTOR.GENERAL,
|
||||
);
|
||||
assert.equal(thresholds.minSpread, +(1.5 * 0.8).toFixed(2)); // 1.20
|
||||
});
|
||||
|
||||
test('bond inflated minSpread = igSpread × 0.90 in HIGH rate regime', () => {
|
||||
const { thresholds } = regime({ igSpread: 1.5 }, { rateRegime: 'HIGH' }).getInflatedOverrides(
|
||||
ASSET_TYPE.BOND,
|
||||
SECTOR.GENERAL,
|
||||
);
|
||||
assert.equal(thresholds.minSpread, +(1.5 * 0.9).toFixed(2)); // 1.35
|
||||
});
|
||||
|
||||
test('GENERAL stock P/E multiplier compresses to 1.2× in HIGH rate regime', () => {
|
||||
const { gates } = regime({ marketPE: 25 }, { rateRegime: 'HIGH' }).getInflatedOverrides(
|
||||
ASSET_TYPE.STOCK,
|
||||
SECTOR.GENERAL,
|
||||
);
|
||||
assert.equal(gates.maxPERatio, Math.round(25 * 1.2)); // 30
|
||||
});
|
||||
|
||||
test('ETF inflated loosens expense gate to 0.75', () => {
|
||||
const { gates } = regime({}).getInflatedOverrides(ASSET_TYPE.ETF);
|
||||
assert.equal(gates.maxExpenseRatio, 0.75);
|
||||
|
||||
@@ -47,3 +47,46 @@ test('_cryptoAdvice: >100% gain → Consider taking profits', () => {
|
||||
const { action } = advisor._cryptoAdvice({ costBasis: 10000, shares: 1 }, 25000);
|
||||
assert.equal(action, '🟠 Consider taking profits');
|
||||
});
|
||||
|
||||
// ── Result map dot-notation normalisation (BRK.B / BRK-B) ───────────────────
|
||||
|
||||
test('advise: BRK-B screener result matches BRK.B holding', async () => {
|
||||
const mockResult = {
|
||||
asset: { ticker: 'BRK-B', currentPrice: 500 },
|
||||
signal: SIGNAL.STRONG_BUY,
|
||||
inflated: { label: '🟢 BUY (High Conviction)' },
|
||||
fundamental: { label: '🟢 BUY (High Conviction)' },
|
||||
};
|
||||
const screenedResults = { STOCK: [mockResult], ETF: [], BOND: [] };
|
||||
const holding = {
|
||||
ticker: 'BRK.B',
|
||||
shares: 1,
|
||||
costBasis: 400,
|
||||
type: 'stock',
|
||||
source: 'Robinhood',
|
||||
};
|
||||
|
||||
const advice = await advisor.advise([holding], screenedResults);
|
||||
// Should match and return a real signal, not "Not screened"
|
||||
assert.equal(advice[0].signal, SIGNAL.STRONG_BUY);
|
||||
});
|
||||
|
||||
test('advise: BRK.B screener result matches BRK-B holding', async () => {
|
||||
const mockResult = {
|
||||
asset: { ticker: 'BRK.B', currentPrice: 500 },
|
||||
signal: SIGNAL.STRONG_BUY,
|
||||
inflated: { label: '🟢 BUY (High Conviction)' },
|
||||
fundamental: { label: '🟢 BUY (High Conviction)' },
|
||||
};
|
||||
const screenedResults = { STOCK: [mockResult], ETF: [], BOND: [] };
|
||||
const holding = {
|
||||
ticker: 'BRK-B',
|
||||
shares: 1,
|
||||
costBasis: 400,
|
||||
type: 'stock',
|
||||
source: 'Robinhood',
|
||||
};
|
||||
|
||||
const advice = await advisor.advise([holding], screenedResults);
|
||||
assert.equal(advice[0].signal, SIGNAL.STRONG_BUY);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user