phase-10.5: screener enhancements
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { DigestService } from '../server/domains/digest/DigestService.js';
|
||||
import { DiscordNotifier } from '../server/domains/digest/DiscordNotifier.js';
|
||||
import type { SignalSnapshotRepository } from '../server/domains/shared/persistence/SignalSnapshotRepository.js';
|
||||
import type { NewsRepository } from '../server/domains/news/NewsRepository.js';
|
||||
import type { NewsArticleRow, SignalSnapshotRow } from '../server/domains/shared/types/index.js';
|
||||
|
||||
function snap(over: Partial<SignalSnapshotRow>): SignalSnapshotRow {
|
||||
return {
|
||||
ticker: 'AAPL',
|
||||
snapshot_date: '2026-06-09',
|
||||
asset_type: 'STOCK',
|
||||
price: 189.5,
|
||||
signal: '✅ Strong Buy',
|
||||
fundamental_tier: 'PASS',
|
||||
fundamental_score: 9,
|
||||
fundamental_label: '🟢 BUY (High Conviction)',
|
||||
inflated_tier: 'PASS',
|
||||
inflated_score: 9,
|
||||
inflated_label: '🟢 BUY (High Conviction)',
|
||||
coverage_active: 8,
|
||||
coverage_total: 11,
|
||||
risk_flags: null,
|
||||
rate_regime: 'NORMAL',
|
||||
created_at: '2026-06-09T21:00:00.000Z',
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function article(over: Partial<NewsArticleRow>): NewsArticleRow {
|
||||
return {
|
||||
url_hash: 'h1',
|
||||
title_hash: 't1',
|
||||
ticker_list: '["AAPL"]',
|
||||
headline: '8-K filing: APPLE INC',
|
||||
body: null,
|
||||
source: 'edgar',
|
||||
catalyst: 'regulatory',
|
||||
url: 'https://sec.gov/x',
|
||||
published_at: '2026-06-08T20:00:00.000Z',
|
||||
created_at: '2026-06-08T20:01:00.000Z',
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function makeService(
|
||||
today: SignalSnapshotRow[],
|
||||
prev: SignalSnapshotRow[],
|
||||
newsByTicker: Record<string, NewsArticleRow[]> = {},
|
||||
): DigestService {
|
||||
const snapshots = {
|
||||
byDate: () => today,
|
||||
latestBefore: () => prev,
|
||||
} as unknown as SignalSnapshotRepository;
|
||||
const news = {
|
||||
newsForTicker: (t: string) => newsByTicker[t] ?? [],
|
||||
} as unknown as NewsRepository;
|
||||
return new DigestService(snapshots, news);
|
||||
}
|
||||
|
||||
test('DigestService', async (t) => {
|
||||
await t.test('detects signal change and attaches catalysts', () => {
|
||||
const service = makeService(
|
||||
[snap({ signal: '🔄 Neutral', fundamental_score: 2 })],
|
||||
[snap({ snapshot_date: '2026-06-08', signal: '✅ Strong Buy', fundamental_score: 9 })],
|
||||
{ AAPL: [article({})] },
|
||||
);
|
||||
const report = service.build('2026-06-09');
|
||||
assert.equal(report.changes.length, 1);
|
||||
const c = report.changes[0];
|
||||
assert.equal(c.previousSignal, '✅ Strong Buy');
|
||||
assert.equal(c.newSignal, '🔄 Neutral');
|
||||
assert.equal(c.scoreDelta, -7);
|
||||
assert.equal(c.catalysts.length, 1);
|
||||
assert.equal(c.catalysts[0].catalyst, 'regulatory');
|
||||
});
|
||||
|
||||
await t.test('no change → empty digest', () => {
|
||||
const service = makeService([snap({})], [snap({ snapshot_date: '2026-06-08' })]);
|
||||
const report = service.build('2026-06-09');
|
||||
assert.equal(report.changes.length, 0);
|
||||
assert.equal(report.snapshotCount, 1);
|
||||
});
|
||||
|
||||
await t.test('first-ever snapshot lands in newTickers, not changes', () => {
|
||||
const service = makeService([snap({ ticker: 'NVDA' })], []);
|
||||
const report = service.build('2026-06-09');
|
||||
assert.equal(report.changes.length, 0);
|
||||
assert.deepEqual(report.newTickers, ['NVDA']);
|
||||
});
|
||||
|
||||
await t.test('M&A stories surface even without a signal change', () => {
|
||||
const service = makeService(
|
||||
[snap({})],
|
||||
[snap({ snapshot_date: '2026-06-08' })], // same signal — no change
|
||||
{
|
||||
AAPL: [
|
||||
article({
|
||||
catalyst: 'ma',
|
||||
headline: 'SC 13D filing: APPLE INC',
|
||||
url_hash: 'h2',
|
||||
url: 'https://sec.gov/13d',
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
const report = service.build('2026-06-09');
|
||||
assert.equal(report.changes.length, 0);
|
||||
assert.equal(report.maStories.length, 1);
|
||||
assert.ok(report.maStories[0].headline.includes('SC 13D'));
|
||||
});
|
||||
|
||||
await t.test('sorts changes by signal-distance impact', () => {
|
||||
const service = makeService(
|
||||
[
|
||||
snap({ ticker: 'SMALL', signal: '⚡ Momentum' }), // Strong Buy(0) → Momentum(1): impact 1
|
||||
snap({ ticker: 'BIG', signal: '❌ Avoid' }), // Strong Buy(0) → Avoid(4): impact 4
|
||||
],
|
||||
[
|
||||
snap({ ticker: 'SMALL', snapshot_date: '2026-06-08', signal: '✅ Strong Buy' }),
|
||||
snap({ ticker: 'BIG', snapshot_date: '2026-06-08', signal: '✅ Strong Buy' }),
|
||||
],
|
||||
);
|
||||
const report = service.build('2026-06-09');
|
||||
assert.equal(report.changes[0].ticker, 'BIG');
|
||||
assert.equal(report.changes[1].ticker, 'SMALL');
|
||||
});
|
||||
});
|
||||
|
||||
test('DiscordNotifier.buildPayload', async (t) => {
|
||||
await t.test('returns null when nothing to report', () => {
|
||||
assert.equal(
|
||||
DiscordNotifier.buildPayload({
|
||||
date: '2026-06-09',
|
||||
changes: [],
|
||||
newTickers: [],
|
||||
maStories: [],
|
||||
snapshotCount: 5,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
await t.test('builds embed with change fields and M&A section', () => {
|
||||
const payload = DiscordNotifier.buildPayload({
|
||||
date: '2026-06-09',
|
||||
changes: [
|
||||
{
|
||||
ticker: 'AAPL',
|
||||
previousSignal: '✅ Strong Buy',
|
||||
newSignal: '🔄 Neutral',
|
||||
previousDate: '2026-06-08',
|
||||
scoreDelta: -7,
|
||||
price: 189.5,
|
||||
catalysts: [
|
||||
{
|
||||
headline: '8-K filing: APPLE INC',
|
||||
catalyst: 'regulatory',
|
||||
source: 'edgar',
|
||||
url: 'https://sec.gov/x',
|
||||
publishedAt: '2026-06-08T20:00:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
newTickers: [],
|
||||
maStories: [
|
||||
{
|
||||
headline: 'SC 13D filing: APPLE INC',
|
||||
catalyst: 'ma',
|
||||
source: 'edgar',
|
||||
url: 'https://sec.gov/13d',
|
||||
publishedAt: '2026-06-08T21:00:00.000Z',
|
||||
},
|
||||
],
|
||||
snapshotCount: 12,
|
||||
});
|
||||
assert.ok(payload);
|
||||
const embed = payload.embeds[0] as {
|
||||
title: string;
|
||||
fields: Array<{ name: string; value: string }>;
|
||||
};
|
||||
assert.ok(embed.title.includes('2026-06-09'));
|
||||
assert.equal(embed.fields.length, 2); // 1 change + 1 M&A section
|
||||
assert.ok(embed.fields[0].name.includes('AAPL'));
|
||||
assert.ok(embed.fields[0].name.includes('score -7'));
|
||||
assert.ok(embed.fields[0].value.includes('regulatory'));
|
||||
assert.ok(embed.fields[1].name.includes('M&A'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user