66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { EtfScorer } from '../server/scorers/EtfScorer';
|
|
import type { EtfMetrics } from '../server/types';
|
|
|
|
const rules = {
|
|
gates: { maxExpenseRatio: 0.5 },
|
|
weights: { yield: 2, lowCost: 3 },
|
|
thresholds: { minYield: 1.5, maxExpense: 0.1, minVolume: 500000 },
|
|
};
|
|
|
|
// Helper to build minimal EtfMetrics fixtures (totalAssets/fiveYearReturn unused by scorer).
|
|
const etf = (partial: Partial<EtfMetrics>): EtfMetrics => ({
|
|
totalAssets: 0,
|
|
fiveYearReturn: 0,
|
|
volume: 0,
|
|
yield: 0,
|
|
expenseRatio: 0,
|
|
...partial,
|
|
});
|
|
|
|
test('rejects ETF with expense ratio above gate', () => {
|
|
const result = EtfScorer.score(etf({ expenseRatio: 0.8, yield: 2.0 }), rules);
|
|
assert.equal(result.label, '🔴 REJECT');
|
|
});
|
|
|
|
test('efficient label for low-cost, high-yield ETF', () => {
|
|
const result = EtfScorer.score(etf({ expenseRatio: 0.03, yield: 2.0, volume: 1000000 }), rules);
|
|
assert.equal(result.label, '🟢 Efficient');
|
|
});
|
|
|
|
test('neutral when yield is below threshold', () => {
|
|
const result = EtfScorer.score(etf({ expenseRatio: 0.03, yield: 0.4, volume: 1000000 }), rules);
|
|
assert.equal(result.label, '🟡 Neutral');
|
|
});
|
|
|
|
test('audit breakdown includes cost, yield, vol keys', () => {
|
|
const result = EtfScorer.score(etf({ expenseRatio: 0.03, yield: 2.0, volume: 1000000 }), rules);
|
|
assert(result.audit.breakdown!.cost != null);
|
|
assert(result.audit.breakdown!.yield != null);
|
|
assert(result.audit.breakdown!.vol != null);
|
|
});
|
|
|
|
test('penalises ETF with volume below liquidity floor', () => {
|
|
const result = EtfScorer.score(etf({ 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(
|
|
etf({ expenseRatio: 0.03, yield: 2.0, volume: 1000000, fiveYearReturn: 10 }),
|
|
rulesWithReturn,
|
|
);
|
|
const poor = EtfScorer.score(
|
|
etf({ 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');
|
|
});
|