File size: 3,267 Bytes
f82ea23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import test from 'node:test';
import assert from 'node:assert/strict';
import { runSignalBacktest, runTechnicalSignalBacktest, BacktestInput } from '../lib/backtest';
import { WeeklyQuote } from '../lib/signal-engine';

function weeklyQuote(week: number, close: number, overrides: Partial<WeeklyQuote> = {}): WeeklyQuote {
  return {
    date: new Date(Date.UTC(2020, 0, 3 + week * 7)),
    open: close - 1,
    high: close + 1,
    low: close - 1,
    close,
    ...overrides,
  };
}

function weekIso(week: number): string {
  const date = weeklyQuote(week, 0).date;
  assert.ok(date instanceof Date);
  return date.toISOString();
}

function inputWithFundamentals(asOf: string): BacktestInput {
  const bars = Array.from({ length: 210 }, (_, index) => weeklyQuote(index, 100));
  bars[204] = weeklyQuote(204, 101, { open: 98, low: 99, high: 102 });
  bars[205] = weeklyQuote(205, 103);
  bars[209] = weeklyQuote(209, 110);

  return {
    symbols: [
      {
        symbol: 'TEST',
        quotes: bars,
        fundamentals: [
          { asOf: new Date('2023-01-01T00:00:00.000Z'), roe: 0.05, debtToEquity: 10, marketCap: 25_000_000_000 },
          { asOf: new Date(asOf), roe: 0.25, debtToEquity: 10, marketCap: 25_000_000_000 },
        ],
      },
    ],
    horizonsWeeks: [4],
  };
}

test('runSignalBacktest does not use fundamentals published after the signal date', () => {
  const result = runSignalBacktest(inputWithFundamentals('2024-01-01T00:00:00.000Z'));

  assert.equal(result.events.length, 0);
});

test('runSignalBacktest labels 4-week outcomes using only bars after the signal date', () => {
  const result = runSignalBacktest(inputWithFundamentals('2023-11-01T00:00:00.000Z'));

  assert.equal(result.events.length, 1);
  const event = result.events[0];
  assert.ok(event);
  assert.equal(event.symbol, 'TEST');
  assert.equal(event.signalDate.toISOString(), weekIso(204));
  assert.equal(event.entryDate.toISOString(), weekIso(205));
  assert.equal(event.horizonWeeks, 4);
  assert.equal(event.exitDate.toISOString(), weekIso(209));
  assert.equal(Number(event.forwardReturn.toFixed(4)), Number(((110 - 103) / 103).toFixed(4)));
  assert.equal(result.summary.eventCount, 1);
  assert.equal(result.summary.winRate, 1);
});

test('runTechnicalSignalBacktest reports quality metrics for 4 and 6 week horizons without fundamentals', () => {
  const bars = Array.from({ length: 214 }, (_, index) => weeklyQuote(index, 100));
  bars[204] = weeklyQuote(204, 101, { open: 98, low: 99, high: 102 });
  bars[205] = weeklyQuote(205, 103);
  bars[209] = weeklyQuote(209, 110);
  bars[211] = weeklyQuote(211, 99);

  const result = runTechnicalSignalBacktest({
    symbols: [{ symbol: 'TECH', quotes: bars }],
    horizonsWeeks: [4, 6],
  });

  assert.equal(result.summary.eventCount, 2);
  assert.equal(result.summary.byHorizon[4].eventCount, 1);
  assert.equal(result.summary.byHorizon[4].winRate, 1);
  assert.equal(Number(result.summary.byHorizon[4].meanReturn.toFixed(4)), Number(((110 - 103) / 103).toFixed(4)));
  assert.equal(result.summary.byHorizon[6].eventCount, 1);
  assert.equal(result.summary.byHorizon[6].winRate, 0);
  assert.equal(Number(result.summary.byHorizon[6].meanReturn.toFixed(4)), Number(((99 - 103) / 103).toFixed(4)));
});