fix bruno collection
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EtfScorer } from '../server/domains/screener/scorers/EtfScorer.js';
|
||||
import { ScoringConfig } from '../server/domains/shared/scoring/ScoringConfig.js';
|
||||
import type { EtfMetrics } from '../server/domains/shared/types/models.model.js';
|
||||
|
||||
const DEFAULT_RULES = {
|
||||
gates: ScoringConfig.base.gates.ETF,
|
||||
weights: ScoringConfig.base.weights.ETF,
|
||||
thresholds: ScoringConfig.base.thresholds.ETF,
|
||||
};
|
||||
|
||||
test('EtfScorer', async (t) => {
|
||||
await t.test('rejects ETF with high expense ratio', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.8, // Exceeds 0.2% gate
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
assert.ok(result.scoreSummary.includes('Expense ratio'));
|
||||
});
|
||||
|
||||
await t.test('accepts ETF with low expense ratio', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.05, // Well below 0.2% gate
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.notEqual(result.label, '🔴 REJECT');
|
||||
});
|
||||
|
||||
await t.test('rejects ETF with zero expense ratio (data issue)', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0, // Zero treated as missing/invalid
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
// Zero is treated as null/missing data
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
await t.test('rejects ETF with low 5-year return', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 5, // Below 8% gate
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
assert.ok(result.scoreSummary.includes('5-year return'));
|
||||
});
|
||||
|
||||
await t.test('accepts ETF with strong 5-year return', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 12, // Above 8% gate
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.notEqual(result.label, '🔴 REJECT');
|
||||
});
|
||||
|
||||
await t.test('rejects ETF with insufficient volume', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 500000, // Below 1M gate
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
assert.ok(result.scoreSummary.includes('Volume'));
|
||||
});
|
||||
|
||||
await t.test('accepts ETF with strong volume', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 10000000, // Well above 1M gate
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.notEqual(result.label, '🔴 REJECT');
|
||||
});
|
||||
|
||||
await t.test('scores high-quality ETF positively', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.05, // Low
|
||||
yield: 3.0, // Decent
|
||||
volume: 20000000, // High
|
||||
fiveYearReturn: 15, // Strong
|
||||
totalAssets: 50e9, // Large
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.notEqual(result.label, '🔴 REJECT');
|
||||
assert.ok(result.audit?.passedGates);
|
||||
});
|
||||
|
||||
await t.test('handles null/undefined metrics gracefully', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: null,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: null,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
// Should not crash, null values are skipped
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
await t.test('includes audit trail of gate checks', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.8,
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.ok(result.audit);
|
||||
if (!result.audit?.passedGates) {
|
||||
assert.ok(Array.isArray(result.audit?.failures));
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('scores yield as secondary factor', () => {
|
||||
const metricsLowYield: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 0.5, // Low yield
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const metricsHighYield: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 4.0, // High yield
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const resultLow = EtfScorer.score(metricsLowYield, DEFAULT_RULES);
|
||||
const resultHigh = EtfScorer.score(metricsHighYield, DEFAULT_RULES);
|
||||
|
||||
// Both should pass gates
|
||||
assert.ok(resultLow.audit?.passedGates);
|
||||
assert.ok(resultHigh.audit?.passedGates);
|
||||
});
|
||||
|
||||
await t.test('rejects ETF with multiple gate failures', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 1.0, // High
|
||||
yield: 0.5,
|
||||
volume: 100000, // Low
|
||||
fiveYearReturn: 3, // Low
|
||||
totalAssets: 100e6, // Small
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
// Should have multiple failures
|
||||
if (!result.audit?.passedGates && result.audit?.failures) {
|
||||
assert.ok(result.audit.failures.length > 1);
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('calculates score based on thresholds', () => {
|
||||
const metricsLowReturn: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 8.5, // Just above gate
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const metricsHighReturn: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 15, // Well above gate
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const resultLow = EtfScorer.score(metricsLowReturn, DEFAULT_RULES);
|
||||
const resultHigh = EtfScorer.score(metricsHighReturn, DEFAULT_RULES);
|
||||
|
||||
// Both should pass
|
||||
assert.ok(resultLow.audit?.passedGates);
|
||||
assert.ok(resultHigh.audit?.passedGates);
|
||||
});
|
||||
|
||||
await t.test('penalizes low-volume ETF', () => {
|
||||
const metricsLowVolume: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 2000000, // Low but above gate
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const metricsHighVolume: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 50000000, // High volume
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const resultLow = EtfScorer.score(metricsLowVolume, DEFAULT_RULES);
|
||||
const resultHigh = EtfScorer.score(metricsHighVolume, DEFAULT_RULES);
|
||||
|
||||
// Both pass gates, but high volume should score higher
|
||||
assert.ok(resultLow.audit?.passedGates);
|
||||
assert.ok(resultHigh.audit?.passedGates);
|
||||
});
|
||||
|
||||
await t.test('handles extremely high expense ratio', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 5.0, // 5%!
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: 10,
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
});
|
||||
|
||||
await t.test('handles negative 5-year return', () => {
|
||||
const metrics: EtfMetrics = {
|
||||
expenseRatio: 0.1,
|
||||
yield: 2.5,
|
||||
volume: 5000000,
|
||||
fiveYearReturn: -5, // Negative return
|
||||
totalAssets: 5e9,
|
||||
};
|
||||
|
||||
const result = EtfScorer.score(metrics, DEFAULT_RULES);
|
||||
assert.equal(result.label, '🔴 REJECT');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user