70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
import { test } from 'node:test';
|
|||
|
|
import { strict as assert } from 'node:assert';
|
||
|
|
import {
|
||
|
|
monthlyReturnsFromCandles,
|
||
|
|
aggregateMonthSeasonality,
|
||
|
|
buildSeasonalitySnapshot,
|
||
|
|
electionCycleYear,
|
||
|
|
upcomingSimpleEvents,
|
||
|
|
} from '../seasonality.ts';
|
||
|
|
|
||
|
|
function mkYear(year: number, monthlyCloses: number[]) {
|
||
|
|
// One candle per month end-ish
|
||
|
|
return monthlyCloses.map((c, i) => ({
|
||
|
|
ts: new Date(Date.UTC(year, i, 28)).toISOString(),
|
||
|
|
c,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
test('monthlyReturnsFromCandles skips incomplete current month', () => {
|
||
|
|
// Use fixed "now" via candles only in past years
|
||
|
|
const candles = [
|
||
|
|
...mkYear(2020, [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]),
|
||
|
|
...mkYear(2021, [111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122]),
|
||
|
|
];
|
||
|
|
const monthly = monthlyReturnsFromCandles(candles, new Date(Date.UTC(2026, 0, 15)));
|
||
|
|
assert.ok(monthly.length >= 20);
|
||
|
|
assert.ok(monthly.every((m) => Number.isFinite(m.returnPct)));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('aggregateMonthSeasonality has 12 months', () => {
|
||
|
|
const monthly = [
|
||
|
|
{ year: 2020, month: 1, returnPct: 2 },
|
||
|
|
{ year: 2021, month: 1, returnPct: -1 },
|
||
|
|
{ year: 2020, month: 5, returnPct: -3 },
|
||
|
|
];
|
||
|
|
const agg = aggregateMonthSeasonality(monthly);
|
||
|
|
assert.equal(agg.length, 12);
|
||
|
|
assert.equal(agg[0].sampleYears, 2);
|
||
|
|
assert.ok(Math.abs(agg[0].avgReturnPct - 0.5) < 0.01);
|
||
|
|
assert.equal(agg[0].winRate, 0.5);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('electionCycleYear labels 2026 as midterm year', () => {
|
||
|
|
const c = electionCycleYear(2026);
|
||
|
|
assert.equal(c.yearInCycle, 2);
|
||
|
|
assert.ok(/midterm/i.test(c.label));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('buildSeasonalitySnapshot marks current month', () => {
|
||
|
|
const candles = [
|
||
|
|
...mkYear(2019, Array.from({ length: 12 }, (_, i) => 100 + i)),
|
||
|
|
...mkYear(2020, Array.from({ length: 12 }, (_, i) => 110 + i)),
|
||
|
|
...mkYear(2021, Array.from({ length: 12 }, (_, i) => 120 + i)),
|
||
|
|
];
|
||
|
|
const snap = buildSeasonalitySnapshot('SPY', candles, new Date(Date.UTC(2026, 6, 15)));
|
||
|
|
assert.equal(snap.currentMonth, 7);
|
||
|
|
assert.equal(snap.symbol, 'SPY');
|
||
|
|
assert.ok(snap.months.length === 12);
|
||
|
|
assert.ok(snap.electionCycle.label.length > 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('upcomingSimpleEvents uses plain language', () => {
|
||
|
|
const events = upcomingSimpleEvents();
|
||
|
|
assert.ok(events.length >= 3);
|
||
|
|
for (const e of events) {
|
||
|
|
assert.ok(e.plainWhy.length > 20);
|
||
|
|
assert.ok(!/you should buy|go long/i.test(e.plainWhy));
|
||
|
|
}
|
||
|
|
});
|