Brajmovech commited on
Commit
d7022e8
·
1 Parent(s): d54c280

Add Almanac accuracy seed inputs

Browse files
data/historical/DJI_daily.csv ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Date","Open","High","Low","Close"
2
+ "12/29/2025","48,636.63","48,704.83","48,390.91","48,461.93"
3
+ "12/30/2025","48,434.88","48,471.70","48,297.26","48,367.06"
4
+ "12/31/2025","48,371.52","48,394.51","48,050.88","48,063.29"
5
+ "01/02/2026","48,105.98","48,404.06","47,853.04","48,382.39"
6
+ "01/05/2026","48,475.81","49,209.95","48,449.62","48,977.18"
7
+ "01/06/2026","48,987.36","49,509.92","48,923.83","49,462.08"
8
+ "01/07/2026","49,512.72","49,621.43","48,951.99","48,996.08"
9
+ "01/08/2026","48,850.17","49,357.74","48,792.34","49,266.11"
10
+ "01/09/2026","49,234.81","49,571.41","49,197.06","49,504.07"
11
+ "01/12/2026","49,499.67","49,633.35","49,011.31","49,590.20"
12
+ "01/13/2026","49,616.95","49,616.95","49,056.31","49,191.99"
13
+ "01/14/2026","49,088.25","49,195.10","48,851.98","49,149.63"
14
+ "01/15/2026","49,201.10","49,581.18","49,201.10","49,442.44"
15
+ "01/16/2026","49,466.70","49,616.70","49,246.24","49,359.33"
16
+ "01/20/2026","49,005.01","49,005.01","48,428.13","48,488.59"
17
+ "01/21/2026","48,546.03","49,295.03","48,546.03","49,077.23"
18
+ "01/22/2026","49,201.81","49,607.29","49,201.81","49,384.01"
19
+ "01/23/2026","49,264.54","49,265.46","48,963.05","49,098.71"
20
+ "01/26/2026","49,137.65","49,488.81","49,137.65","49,412.40"
21
+ "01/27/2026","49,103.58","49,157.80","48,862.52","49,003.41"
22
+ "01/28/2026","49,024.68","49,150.34","48,901.49","49,015.60"
23
+ "01/29/2026","48,938.27","49,292.81","48,597.22","49,071.56"
24
+ "01/30/2026","48,991.62","49,047.68","48,459.88","48,892.47"
25
+ "02/02/2026","48,777.77","49,484.95","48,673.58","49,407.66"
26
+ "02/03/2026","49,358.59","49,653.13","48,832.78","49,240.99"
27
+ "02/04/2026","49,323.59","49,649.86","49,112.43","49,501.30"
28
+ "02/05/2026","49,313.04","49,340.90","48,829.10","48,908.72"
29
+ "02/06/2026","49,032.19","50,169.65","49,032.19","50,115.67"
30
+ "02/09/2026","50,047.79","50,219.40","49,837.45","50,135.87"
31
+ "02/10/2026","50,193.49","50,512.79","50,115.03","50,188.14"
32
+ "02/11/2026","50,243.15","50,499.04","49,901.61","50,121.40"
33
+ "02/12/2026","50,170.27","50,447.01","49,420.28","49,451.98"
34
+ "02/13/2026","49,439.58","49,743.98","49,084.35","49,500.93"
35
+ "02/17/2026","49,525.37","49,732.37","49,169.84","49,533.19"
36
+ "02/18/2026","49,571.92","49,897.31","49,469.06","49,662.66"
37
+ "02/19/2026","49,576.22","49,606.17","49,197.53","49,395.16"
38
+ "02/20/2026","49,323.00","49,712.56","49,158.28","49,625.97"
39
+ "02/23/2026","49,536.54","49,695.61","48,731.46","48,804.06"
40
+ "02/24/2026","48,827.80","49,295.21","48,752.74","49,174.50"
41
+ "02/25/2026","49,357.63","49,517.36","49,206.87","49,482.15"
42
+ "02/26/2026","49,544.58","49,815.22","49,237.38","49,499.20"
43
+ "02/27/2026","49,253.57","49,253.57","48,678.78","48,977.92"
44
+ "03/02/2026","48,794.42","49,064.67","48,377.96","48,904.78"
45
+ "03/03/2026","48,493.11","48,695.36","47,626.85","48,501.27"
46
+ "03/04/2026","48,589.77","48,854.05","48,354.37","48,739.41"
47
+ "03/05/2026","48,526.73","48,526.73","47,577.11","47,954.74"
48
+ "03/06/2026","47,634.55","47,634.55","47,009.01","47,501.55"
49
+ "03/09/2026","47,371.28","47,876.06","46,615.52","47,740.80"
50
+ "03/10/2026","47,771.43","48,220.54","47,444.23","47,706.51"
51
+ "03/11/2026","47,690.76","47,711.26","47,185.89","47,417.27"
52
+ "03/12/2026","47,242.52","47,242.52","46,662.23","46,677.85"
53
+ "03/13/2026","46,689.24","47,123.99","46,494.63","46,558.47"
54
+ "03/16/2026","46,707.40","47,176.14","46,707.40","46,946.41"
55
+ "03/17/2026","47,085.53","47,428.12","46,975.52","46,993.26"
56
+ "03/18/2026","46,913.93","46,913.93","46,193.06","46,225.15"
57
+ "03/19/2026","46,134.87","46,247.22","45,733.70","46,021.43"
58
+ "03/20/2026","45,975.65","46,068.31","45,369.39","45,577.47"
59
+ "03/23/2026","45,803.82","46,712.33","45,803.82","46,208.47"
60
+ "03/24/2026","46,099.86","46,400.82","45,769.69","46,124.06"
61
+ "03/25/2026","46,314.24","46,718.42","46,196.91","46,429.49"
62
+ "03/26/2026","46,344.64","46,547.59","45,910.75","45,960.11"
63
+ "03/27/2026","45,904.25","45,904.25","45,063.33","45,166.64"
64
+ "03/30/2026","45,283.06","45,625.76","45,057.28","45,216.14"
65
+ "03/31/2026","45,541.76","46,383.40","45,480.30","46,341.51"
66
+ "04/01/2026","46,396.12","46,803.36","46,396.12","46,565.74"
67
+ "04/02/2026","46,469.36","46,754.72","45,897.24","46,504.67"
data/historical/GSPC_daily.csv ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Date","Open","High","Low","Close"
2
+ "12/29/2025","6,903.60","6,920.21","6,888.76","6,905.74"
3
+ "12/30/2025","6,900.44","6,913.25","6,893.47","6,896.24"
4
+ "12/31/2025","6,898.82","6,901.42","6,844.55","6,845.50"
5
+ "01/02/2026","6,878.11","6,894.87","6,824.31","6,858.47"
6
+ "01/05/2026","6,892.19","6,920.38","6,891.56","6,902.05"
7
+ "01/06/2026","6,908.03","6,948.69","6,904.02","6,944.82"
8
+ "01/07/2026","6,945.07","6,965.69","6,919.19","6,920.93"
9
+ "01/08/2026","6,914.11","6,931.28","6,899.33","6,921.46"
10
+ "01/09/2026","6,927.83","6,978.36","6,917.64","6,966.28"
11
+ "01/12/2026","6,944.12","6,986.33","6,934.07","6,977.27"
12
+ "01/13/2026","6,977.41","6,985.83","6,938.77","6,963.74"
13
+ "01/14/2026","6,937.41","6,941.30","6,885.74","6,926.60"
14
+ "01/15/2026","6,969.46","6,979.34","6,937.93","6,944.47"
15
+ "01/16/2026","6,960.54","6,967.30","6,925.09","6,940.01"
16
+ "01/20/2026","6,865.24","6,871.17","6,789.05","6,796.86"
17
+ "01/21/2026","6,810.71","6,910.39","6,804.96","6,875.62"
18
+ "01/22/2026","6,914.44","6,934.75","6,893.62","6,913.35"
19
+ "01/23/2026","6,907.85","6,932.96","6,895.50","6,915.61"
20
+ "01/26/2026","6,923.23","6,964.66","6,921.60","6,950.23"
21
+ "01/27/2026","6,965.96","6,988.82","6,958.83","6,978.60"
22
+ "01/28/2026","7,002.00","7,002.28","6,963.46","6,978.03"
23
+ "01/29/2026","6,977.74","6,992.84","6,870.80","6,969.01"
24
+ "01/30/2026","6,947.27","6,964.09","6,893.48","6,939.03"
25
+ "02/02/2026","6,916.64","6,991.92","6,914.34","6,976.44"
26
+ "02/03/2026","6,985.45","6,993.08","6,862.05","6,917.81"
27
+ "02/04/2026","6,924.50","6,936.09","6,838.80","6,882.72"
28
+ "02/05/2026","6,837.39","6,857.85","6,780.13","6,798.40"
29
+ "02/06/2026","6,816.74","6,944.89","6,816.74","6,932.30"
30
+ "02/09/2026","6,917.26","6,980.10","6,905.87","6,964.82"
31
+ "02/10/2026","6,974.49","6,986.83","6,937.53","6,941.81"
32
+ "02/11/2026","6,976.48","6,993.48","6,911.97","6,941.47"
33
+ "02/12/2026","6,957.54","6,973.22","6,824.04","6,832.76"
34
+ "02/13/2026","6,834.27","6,881.96","6,794.55","6,836.17"
35
+ "02/17/2026","6,819.86","6,866.99","6,775.50","6,843.22"
36
+ "02/18/2026","6,855.48","6,909.12","6,849.66","6,881.31"
37
+ "02/19/2026","6,861.34","6,879.12","6,833.06","6,861.89"
38
+ "02/20/2026","6,843.26","6,915.86","6,836.33","6,909.51"
39
+ "02/23/2026","6,901.25","6,916.96","6,819.82","6,837.75"
40
+ "02/24/2026","6,837.37","6,899.17","6,815.43","6,890.07"
41
+ "02/25/2026","6,915.15","6,952.51","6,915.15","6,946.13"
42
+ "02/26/2026","6,944.74","6,947.25","6,859.73","6,908.86"
43
+ "02/27/2026","6,856.54","6,882.96","6,831.74","6,878.88"
44
+ "03/02/2026","6,824.36","6,901.01","6,796.85","6,881.62"
45
+ "03/03/2026","6,800.26","6,840.05","6,710.42","6,816.63"
46
+ "03/04/2026","6,831.69","6,885.94","6,811.64","6,869.50"
47
+ "03/05/2026","6,851.08","6,870.43","6,770.78","6,830.71"
48
+ "03/06/2026","6,769.03","6,773.42","6,711.56","6,740.02"
49
+ "03/09/2026","6,699.80","6,810.44","6,636.04","6,795.99"
50
+ "03/10/2026","6,796.56","6,845.08","6,759.74","6,781.48"
51
+ "03/11/2026","6,790.09","6,811.15","6,745.59","6,775.80"
52
+ "03/12/2026","6,740.88","6,740.88","6,670.40","6,672.62"
53
+ "03/13/2026","6,673.49","6,733.30","6,623.92","6,632.19"
54
+ "03/16/2026","6,674.37","6,729.79","6,674.37","6,699.38"
55
+ "03/17/2026","6,722.35","6,754.30","6,710.80","6,716.09"
56
+ "03/18/2026","6,697.16","6,705.18","6,621.66","6,624.70"
57
+ "03/19/2026","6,583.12","6,636.74","6,557.82","6,606.49"
58
+ "03/20/2026","6,594.66","6,594.66","6,473.52","6,506.48"
59
+ "03/23/2026","6,574.96","6,651.62","6,565.55","6,581.00"
60
+ "03/24/2026","6,552.09","6,595.75","6,525.11","6,556.37"
61
+ "03/25/2026","6,598.35","6,633.94","6,568.41","6,591.90"
62
+ "03/26/2026","6,555.86","6,573.22","6,473.79","6,477.16"
63
+ "03/27/2026","6,453.89","6,453.89","6,356.08","6,368.85"
64
+ "03/30/2026","6,403.37","6,427.31","6,316.91","6,343.72"
65
+ "03/31/2026","6,395.88","6,539.05","6,395.88","6,528.52"
66
+ "04/01/2026","6,556.56","6,609.67","6,554.29","6,575.32"
67
+ "04/02/2026","6,512.61","6,601.91","6,474.94","6,582.69"
data/historical/IXIC_daily.csv ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Date","Open","High","Low","Close"
2
+ "12/29/2025","23,414.68","23,531.02","23,397.52","23,474.35"
3
+ "12/30/2025","23,465.67","23,521.05","23,414.83","23,419.08"
4
+ "12/31/2025","23,420.85","23,445.26","23,237.78","23,241.99"
5
+ "01/02/2026","23,481.49","23,585.96","23,119.49","23,235.63"
6
+ "01/05/2026","23,449.67","23,476.51","23,332.23","23,395.82"
7
+ "01/06/2026","23,446.96","23,559.15","23,389.57","23,547.17"
8
+ "01/07/2026","23,544.90","23,723.37","23,504.22","23,584.27"
9
+ "01/08/2026","23,548.89","23,558.17","23,353.46","23,480.02"
10
+ "01/09/2026","23,496.21","23,721.15","23,426.48","23,671.35"
11
+ "01/12/2026","23,576.88","23,804.05","23,562.97","23,733.90"
12
+ "01/13/2026","23,735.12","23,813.30","23,607.59","23,709.87"
13
+ "01/14/2026","23,563.92","23,590.20","23,306.66","23,471.75"
14
+ "01/15/2026","23,693.97","23,721.11","23,502.18","23,530.02"
15
+ "01/16/2026","23,639.69","23,664.26","23,446.81","23,515.39"
16
+ "01/20/2026","23,142.69","23,236.05","22,916.83","22,954.32"
17
+ "01/21/2026","23,017.68","23,383.24","22,927.88","23,224.82"
18
+ "01/22/2026","23,440.71","23,503.16","23,335.15","23,436.02"
19
+ "01/23/2026","23,440.92","23,610.74","23,374.26","23,501.24"
20
+ "01/26/2026","23,529.28","23,688.94","23,486.08","23,601.36"
21
+ "01/27/2026","23,734.75","23,865.26","23,694.38","23,817.10"
22
+ "01/28/2026","23,965.11","23,988.27","23,775.49","23,857.45"
23
+ "01/29/2026","23,830.92","23,840.55","23,232.78","23,685.12"
24
+ "01/30/2026","23,578.96","23,662.25","23,351.55","23,461.82"
25
+ "02/02/2026","23,370.55","23,686.83","23,356.40","23,592.11"
26
+ "02/03/2026","23,667.44","23,691.60","23,027.22","23,255.19"
27
+ "02/04/2026","23,217.02","23,270.07","22,684.51","22,904.58"
28
+ "02/05/2026","22,604.02","22,841.28","22,461.14","22,540.59"
29
+ "02/06/2026","22,625.30","23,088.46","22,586.40","23,031.21"
30
+ "02/09/2026","22,952.24","23,314.67","22,878.37","23,238.67"
31
+ "02/10/2026","23,271.23","23,310.73","23,089.10","23,102.47"
32
+ "02/11/2026","23,278.29","23,320.62","22,902.01","23,066.47"
33
+ "02/12/2026","23,142.87","23,161.60","22,548.02","22,597.15"
34
+ "02/13/2026","22,561.46","22,742.06","22,402.38","22,546.67"
35
+ "02/17/2026","22,394.76","22,690.83","22,256.76","22,578.38"
36
+ "02/18/2026","22,629.85","22,895.96","22,597.77","22,753.63"
37
+ "02/19/2026","22,639.88","22,768.83","22,583.61","22,682.73"
38
+ "02/20/2026","22,542.28","22,948.87","22,539.05","22,886.07"
39
+ "02/23/2026","22,840.97","22,893.22","22,547.12","22,627.27"
40
+ "02/24/2026","22,641.60","22,895.48","22,528.26","22,863.68"
41
+ "02/25/2026","23,005.01","23,169.68","23,004.69","23,152.08"
42
+ "02/26/2026","23,100.58","23,109.47","22,670.80","22,878.38"
43
+ "02/27/2026","22,615.43","22,735.78","22,538.30","22,668.21"
44
+ "03/02/2026","22,322.12","22,802.80","22,306.08","22,748.86"
45
+ "03/03/2026","22,292.37","22,601.59","22,124.78","22,516.69"
46
+ "03/04/2026","22,620.89","22,891.88","22,570.67","22,807.48"
47
+ "03/05/2026","22,707.47","22,877.02","22,500.29","22,748.99"
48
+ "03/06/2026","22,421.17","22,614.41","22,328.14","22,387.68"
49
+ "03/09/2026","22,184.05","22,741.03","22,061.97","22,695.95"
50
+ "03/10/2026","22,722.94","22,906.72","22,608.23","22,697.10"
51
+ "03/11/2026","22,771.27","22,877.71","22,602.33","22,716.13"
52
+ "03/12/2026","22,526.59","22,550.75","22,290.48","22,311.98"
53
+ "03/13/2026","22,425.70","22,521.38","22,069.24","22,105.36"
54
+ "03/16/2026","22,340.39","22,521.59","22,316.63","22,374.18"
55
+ "03/17/2026","22,458.03","22,569.64","22,409.07","22,479.53"
56
+ "03/18/2026","22,421.96","22,461.76","22,144.76","22,152.42"
57
+ "03/19/2026","21,871.04","22,187.06","21,851.05","22,090.69"
58
+ "03/20/2026","21,989.33","21,997.09","21,522.75","21,647.61"
59
+ "03/23/2026","21,995.78","22,189.34","21,865.80","21,946.76"
60
+ "03/24/2026","21,807.60","21,916.16","21,712.04","21,761.89"
61
+ "03/25/2026","22,006.43","22,093.18","21,865.46","21,929.83"
62
+ "03/26/2026","21,693.18","21,823.58","21,395.77","21,408.08"
63
+ "03/27/2026","21,287.19","21,293.50","20,909.93","20,948.36"
64
+ "03/30/2026","21,096.24","21,139.72","20,690.25","20,794.64"
65
+ "03/31/2026","21,064.33","21,642.62","21,063.38","21,590.63"
66
+ "04/01/2026","21,742.80","21,983.07","21,723.72","21,840.95"
67
+ "04/02/2026","21,472.52","21,906.48","21,371.32","21,879.18"
scripts/seed_accuracy.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Seed Almanac historic accuracy data from local index CSV files.
3
+
4
+ Run this script from the project root so relative paths resolve against the repo:
5
+ python scripts/seed_accuracy.py
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import csv
12
+ import json
13
+ import sys
14
+ from collections import defaultdict
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+
18
+
19
+ PRIMARY_ALMANAC_PATH = Path("data") / "almanac_2026" / "almanac_2026.json"
20
+ FALLBACK_ALMANAC_PATH = Path("data") / "almanac_2026" / "almanac_2026_db_dump.json"
21
+ OUTPUT_PATH = Path("data") / "almanac_2026" / "accuracy_results.json"
22
+
23
+ INDEX_CONFIG = {
24
+ "d": {
25
+ "csv_key": "dji",
26
+ "summary_key": "dow",
27
+ "label": "Dow",
28
+ "arg": "dji",
29
+ "default": Path("data") / "historical" / "DJI_daily.csv",
30
+ },
31
+ "s": {
32
+ "csv_key": "sp500",
33
+ "summary_key": "sp500",
34
+ "label": "S&P 500",
35
+ "arg": "sp500",
36
+ "default": Path("data") / "historical" / "GSPC_daily.csv",
37
+ },
38
+ "n": {
39
+ "csv_key": "nasdaq",
40
+ "summary_key": "nasdaq",
41
+ "label": "NASDAQ",
42
+ "arg": "nasdaq",
43
+ "default": Path("data") / "historical" / "IXIC_daily.csv",
44
+ },
45
+ }
46
+
47
+
48
+ def iso_utc_now() -> str:
49
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
50
+
51
+
52
+ def read_json(path: Path) -> dict:
53
+ with path.open("r", encoding="utf-8") as handle:
54
+ payload = json.load(handle)
55
+ if not isinstance(payload, dict):
56
+ raise ValueError(f"{path} must contain a top-level JSON object")
57
+ return payload
58
+
59
+
60
+ def load_almanac_predictions(project_root: Path) -> dict[str, dict[str, object]]:
61
+ primary_path = project_root / PRIMARY_ALMANAC_PATH
62
+ fallback_path = project_root / FALLBACK_ALMANAC_PATH
63
+
64
+ if primary_path.exists():
65
+ payload = read_json(primary_path)
66
+ daily = payload.get("daily", {})
67
+ if isinstance(daily, dict):
68
+ normalized = {}
69
+ for date_key, day in daily.items():
70
+ if not isinstance(day, dict):
71
+ continue
72
+ normalized[str(date_key)] = {
73
+ "d": float(day.get("d", 0.0)),
74
+ "s": float(day.get("s", 0.0)),
75
+ "n": float(day.get("n", 0.0)),
76
+ "context": str(day.get("notes", "") or "").strip(),
77
+ }
78
+ if normalized:
79
+ return normalized
80
+
81
+ if fallback_path.exists():
82
+ payload = read_json(fallback_path)
83
+ table = payload.get("daily_probabilities", {})
84
+ rows = table.get("rows", []) if isinstance(table, dict) else []
85
+ if isinstance(rows, list):
86
+ normalized = {}
87
+ for row in rows:
88
+ if not isinstance(row, dict):
89
+ continue
90
+ date_key = str(row.get("date", "")).strip()
91
+ if not date_key:
92
+ continue
93
+ normalized[date_key] = {
94
+ "d": float(row.get("dow_prob", 0.0)),
95
+ "s": float(row.get("sp500_prob", 0.0)),
96
+ "n": float(row.get("nasdaq_prob", 0.0)),
97
+ "context": str(row.get("notes", "") or "").strip(),
98
+ }
99
+ if normalized:
100
+ return normalized
101
+
102
+ raise FileNotFoundError(
103
+ "No supported almanac source found. Expected "
104
+ f"{primary_path} or {fallback_path}."
105
+ )
106
+
107
+
108
+ def parse_close(value: str) -> float:
109
+ return float(str(value or "").replace(",", "").strip())
110
+
111
+
112
+ def load_history_csv(path: Path) -> dict[str, dict[str, float | None]]:
113
+ if not path.exists():
114
+ raise FileNotFoundError(f"Missing historical CSV: {path}")
115
+
116
+ rows: list[tuple[datetime, float]] = []
117
+ with path.open("r", encoding="utf-8-sig", newline="") as handle:
118
+ reader = csv.DictReader(handle)
119
+ required = {"Date", "Close"}
120
+ if not required.issubset(set(reader.fieldnames or [])):
121
+ raise ValueError(f"{path} must contain Date and Close columns")
122
+ for row in reader:
123
+ date_text = str(row.get("Date", "")).strip()
124
+ if not date_text:
125
+ continue
126
+ try:
127
+ parsed_date = datetime.strptime(date_text, "%m/%d/%Y")
128
+ close_value = parse_close(str(row.get("Close", "")))
129
+ except ValueError as exc:
130
+ raise ValueError(f"Unable to parse row in {path}: {row}") from exc
131
+ rows.append((parsed_date, close_value))
132
+
133
+ if not rows:
134
+ raise ValueError(f"{path} did not contain any historical rows")
135
+
136
+ rows.sort(key=lambda item: item[0])
137
+ lookup: dict[str, dict[str, float | None]] = {}
138
+ previous_close: float | None = None
139
+ for trade_date, close_value in rows:
140
+ iso_date = trade_date.strftime("%Y-%m-%d")
141
+ lookup[iso_date] = {"close": close_value, "prev_close": previous_close}
142
+ previous_close = close_value
143
+ return lookup
144
+
145
+
146
+ def actual_direction(pct_change: float) -> str:
147
+ if pct_change > 0:
148
+ return "UP"
149
+ if pct_change < 0:
150
+ return "DOWN"
151
+ return "FLAT"
152
+
153
+
154
+ def predicted_direction(probability: float) -> str | None:
155
+ if probability > 50:
156
+ return "UP"
157
+ if probability < 50:
158
+ return "DOWN"
159
+ return None
160
+
161
+
162
+ def score_prediction(probability: float, pct_change: float) -> dict[str, str | None]:
163
+ predicted = predicted_direction(probability)
164
+ actual = actual_direction(pct_change)
165
+ verdict = None
166
+
167
+ if predicted == "UP":
168
+ verdict = "HIT" if pct_change > 0 else "MISS"
169
+ elif predicted == "DOWN":
170
+ verdict = "HIT" if pct_change < 0 else "MISS"
171
+
172
+ return {"verdict": verdict, "predicted": predicted, "actual": actual}
173
+
174
+
175
+ def pct(value: int, total: int) -> float:
176
+ if total <= 0:
177
+ return 0.0
178
+ return round((value / total) * 100, 1)
179
+
180
+
181
+ def build_daily_results(
182
+ almanac_daily: dict[str, dict[str, object]],
183
+ history_by_index: dict[str, dict[str, dict[str, float | None]]],
184
+ ) -> dict[str, dict[str, object]]:
185
+ daily_results: dict[str, dict[str, object]] = {}
186
+
187
+ for date_key in sorted(almanac_daily.keys()):
188
+ current_records = {}
189
+ for config in INDEX_CONFIG.values():
190
+ history = history_by_index[config["csv_key"]]
191
+ current_records[config["csv_key"]] = history.get(date_key)
192
+
193
+ if any(record is None or record.get("prev_close") is None for record in current_records.values()):
194
+ continue
195
+
196
+ day_predictions = almanac_daily[date_key]
197
+ actuals = {}
198
+ prev_closes = {}
199
+ pct_changes = {}
200
+ results = {}
201
+ hits = 0
202
+ total_calls = 0
203
+
204
+ for signal_key, config in INDEX_CONFIG.items():
205
+ csv_key = config["csv_key"]
206
+ record = current_records[csv_key] or {}
207
+ close_value = float(record["close"])
208
+ prev_close = float(record["prev_close"])
209
+ pct_change = (close_value - prev_close) / prev_close
210
+ probability = float(day_predictions.get(signal_key, 0.0))
211
+
212
+ actuals[csv_key] = round(close_value, 6)
213
+ prev_closes[csv_key] = round(prev_close, 6)
214
+ pct_changes[csv_key] = round(pct_change, 6)
215
+ results[signal_key] = score_prediction(probability, pct_change)
216
+
217
+ if results[signal_key]["verdict"] is not None:
218
+ total_calls += 1
219
+ if results[signal_key]["verdict"] == "HIT":
220
+ hits += 1
221
+
222
+ daily_results[date_key] = {
223
+ "actual": actuals,
224
+ "prev_close": prev_closes,
225
+ "pct_change": pct_changes,
226
+ "almanac_scores": {
227
+ "d": float(day_predictions.get("d", 0.0)),
228
+ "s": float(day_predictions.get("s", 0.0)),
229
+ "n": float(day_predictions.get("n", 0.0)),
230
+ },
231
+ "results": results,
232
+ "hits": hits,
233
+ "total_calls": total_calls,
234
+ "context": str(day_predictions.get("context", "") or "").strip(),
235
+ }
236
+
237
+ return daily_results
238
+
239
+
240
+ def aggregate_periods(
241
+ daily_results: dict[str, dict[str, object]],
242
+ key_builder,
243
+ include_dates: bool = False,
244
+ include_trading_days: bool = False,
245
+ ) -> dict[str, dict[str, object]]:
246
+ grouped: dict[str, dict[str, object]] = defaultdict(
247
+ lambda: {
248
+ "dates": [],
249
+ "hits": 0,
250
+ "total_calls": 0,
251
+ "dow": {"hits": 0, "total": 0},
252
+ "sp500": {"hits": 0, "total": 0},
253
+ "nasdaq": {"hits": 0, "total": 0},
254
+ }
255
+ )
256
+
257
+ for date_key, day in sorted(daily_results.items()):
258
+ group_key = key_builder(date_key)
259
+ bucket = grouped[group_key]
260
+ bucket["dates"].append(date_key)
261
+ bucket["hits"] += int(day.get("hits", 0))
262
+ bucket["total_calls"] += int(day.get("total_calls", 0))
263
+
264
+ for signal_key, config in INDEX_CONFIG.items():
265
+ result = (day.get("results", {}) or {}).get(signal_key, {})
266
+ verdict = result.get("verdict")
267
+ if verdict is None:
268
+ continue
269
+ summary_bucket = bucket[config["summary_key"]]
270
+ summary_bucket["total"] += 1
271
+ if verdict == "HIT":
272
+ summary_bucket["hits"] += 1
273
+
274
+ summarized: dict[str, dict[str, object]] = {}
275
+ for group_key, bucket in sorted(grouped.items()):
276
+ record: dict[str, object] = {
277
+ "hits": bucket["hits"],
278
+ "total_calls": bucket["total_calls"],
279
+ "accuracy": pct(bucket["hits"], bucket["total_calls"]),
280
+ }
281
+ if include_dates:
282
+ record["dates"] = bucket["dates"]
283
+ for index_key in ("dow", "sp500", "nasdaq"):
284
+ index_bucket = bucket[index_key]
285
+ record[index_key] = {
286
+ "hits": index_bucket["hits"],
287
+ "total": index_bucket["total"],
288
+ "pct": pct(index_bucket["hits"], index_bucket["total"]),
289
+ }
290
+ if include_trading_days:
291
+ record["trading_days"] = len(bucket["dates"])
292
+ summarized[group_key] = record
293
+
294
+ return summarized
295
+
296
+
297
+ def build_output(daily_results: dict[str, dict[str, object]]) -> dict[str, object]:
298
+ weekly = aggregate_periods(
299
+ daily_results,
300
+ key_builder=lambda date_key: datetime.strptime(date_key, "%Y-%m-%d").strftime("%Y-W%W"),
301
+ include_dates=True,
302
+ )
303
+ monthly = aggregate_periods(
304
+ daily_results,
305
+ key_builder=lambda date_key: date_key[:7],
306
+ include_trading_days=True,
307
+ )
308
+
309
+ sorted_dates = sorted(daily_results.keys())
310
+ return {
311
+ "meta": {
312
+ "last_updated": iso_utc_now(),
313
+ "total_days_scored": len(sorted_dates),
314
+ "data_range": {
315
+ "from": sorted_dates[0] if sorted_dates else None,
316
+ "to": sorted_dates[-1] if sorted_dates else None,
317
+ },
318
+ "source": "Historic CSV backtest via scripts/seed_accuracy.py",
319
+ },
320
+ "daily": daily_results,
321
+ "weekly": weekly,
322
+ "monthly": monthly,
323
+ }
324
+
325
+
326
+ def format_score(hits: int, total: int) -> str:
327
+ if total <= 0:
328
+ return "0/0 (--%)"
329
+ return f"{hits}/{total} ({round((hits / total) * 100):.0f}%)"
330
+
331
+
332
+ def print_summary(output: dict[str, object]) -> None:
333
+ monthly = output.get("monthly", {})
334
+ if not isinstance(monthly, dict):
335
+ return
336
+
337
+ print("=== 2026 Almanac Accuracy Backtest ===")
338
+ print(f"{'Month':<10} {'Dow':<15} {'S&P 500':<15} {'NASDAQ':<15} {'All':<15}")
339
+
340
+ total_hits = 0
341
+ total_calls = 0
342
+ per_index_totals = {
343
+ "dow": {"hits": 0, "total": 0},
344
+ "sp500": {"hits": 0, "total": 0},
345
+ "nasdaq": {"hits": 0, "total": 0},
346
+ }
347
+
348
+ for month_key in sorted(monthly.keys()):
349
+ month_data = monthly[month_key]
350
+ month_name = datetime.strptime(month_key + "-01", "%Y-%m-%d").strftime("%B")
351
+ total_hits += int(month_data.get("hits", 0))
352
+ total_calls += int(month_data.get("total_calls", 0))
353
+ for index_key in per_index_totals:
354
+ per_index_totals[index_key]["hits"] += int(month_data.get(index_key, {}).get("hits", 0))
355
+ per_index_totals[index_key]["total"] += int(month_data.get(index_key, {}).get("total", 0))
356
+
357
+ print(
358
+ f"{month_name:<10} "
359
+ f"{format_score(month_data['dow']['hits'], month_data['dow']['total']):<15} "
360
+ f"{format_score(month_data['sp500']['hits'], month_data['sp500']['total']):<15} "
361
+ f"{format_score(month_data['nasdaq']['hits'], month_data['nasdaq']['total']):<15} "
362
+ f"{format_score(month_data['hits'], month_data['total_calls']):<15}"
363
+ )
364
+
365
+ total_label = "Q1 Total" if set(monthly.keys()).issubset({"2026-01", "2026-02", "2026-03"}) else "YTD Total"
366
+ print(
367
+ f"{total_label:<10} "
368
+ f"{format_score(per_index_totals['dow']['hits'], per_index_totals['dow']['total']):<15} "
369
+ f"{format_score(per_index_totals['sp500']['hits'], per_index_totals['sp500']['total']):<15} "
370
+ f"{format_score(per_index_totals['nasdaq']['hits'], per_index_totals['nasdaq']['total']):<15} "
371
+ f"{format_score(total_hits, total_calls):<15}"
372
+ )
373
+
374
+
375
+ def parse_args() -> argparse.Namespace:
376
+ parser = argparse.ArgumentParser(description="Seed Almanac historic accuracy results from local CSV data.")
377
+ parser.add_argument("--dji", type=Path, default=INDEX_CONFIG["d"]["default"])
378
+ parser.add_argument("--sp500", type=Path, default=INDEX_CONFIG["s"]["default"])
379
+ parser.add_argument("--nasdaq", type=Path, default=INDEX_CONFIG["n"]["default"])
380
+ return parser.parse_args()
381
+
382
+
383
+ def main() -> int:
384
+ args = parse_args()
385
+ project_root = Path.cwd()
386
+
387
+ try:
388
+ almanac_daily = load_almanac_predictions(project_root)
389
+ history_by_index = {
390
+ "dji": load_history_csv(project_root / Path(args.dji)),
391
+ "sp500": load_history_csv(project_root / Path(args.sp500)),
392
+ "nasdaq": load_history_csv(project_root / Path(args.nasdaq)),
393
+ }
394
+ daily_results = build_daily_results(almanac_daily, history_by_index)
395
+ output = build_output(daily_results)
396
+
397
+ output_path = project_root / OUTPUT_PATH
398
+ output_path.parent.mkdir(parents=True, exist_ok=True)
399
+ with output_path.open("w", encoding="utf-8") as handle:
400
+ json.dump(output, handle, indent=2)
401
+ handle.write("\n")
402
+
403
+ print_summary(output)
404
+ print(f"Wrote {output_path}")
405
+ return 0
406
+ except Exception as exc:
407
+ print(f"[seed_accuracy] {exc}", file=sys.stderr)
408
+ return 1
409
+
410
+
411
+ if __name__ == "__main__":
412
+ raise SystemExit(main())
tests/test_seed_accuracy.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CLI tests for scripts/seed_accuracy.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import json
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import unittest
12
+ from pathlib import Path
13
+
14
+
15
+ REPO_ROOT = Path(__file__).resolve().parent.parent
16
+ SCRIPT_PATH = REPO_ROOT / "scripts" / "seed_accuracy.py"
17
+
18
+
19
+ def write_csv(path: Path, rows: list[dict[str, str]]) -> None:
20
+ path.parent.mkdir(parents=True, exist_ok=True)
21
+ with path.open("w", encoding="utf-8", newline="") as handle:
22
+ writer = csv.DictWriter(handle, fieldnames=["Date", "Open", "High", "Low", "Close"])
23
+ writer.writeheader()
24
+ writer.writerows(rows)
25
+
26
+
27
+ def write_primary_almanac(path: Path) -> None:
28
+ payload = {
29
+ "meta": {"source": "fixture", "year": 2026, "generated_at": "2026-04-05T00:00:00Z"},
30
+ "months": {},
31
+ "daily": {
32
+ "2026-01-02": {"d": 60.0, "s": 40.0, "n": 50.0, "notes": "Opening session"},
33
+ "2026-01-05": {"d": 45.0, "s": 55.0, "n": 70.0, "notes": ""},
34
+ "2026-01-06": {"d": 80.0, "s": 20.0, "n": 60.0, "notes": "Momentum test"},
35
+ },
36
+ "seasonal_signals": [],
37
+ "seasonal_heatmap": {},
38
+ }
39
+ path.parent.mkdir(parents=True, exist_ok=True)
40
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
41
+
42
+
43
+ def write_fallback_almanac(path: Path) -> None:
44
+ payload = {
45
+ "daily_probabilities": {
46
+ "rows": [
47
+ {"date": "2026-01-02", "dow_prob": 60.0, "sp500_prob": 40.0, "nasdaq_prob": 50.0, "notes": "Opening session"},
48
+ {"date": "2026-01-05", "dow_prob": 45.0, "sp500_prob": 55.0, "nasdaq_prob": 70.0, "notes": ""},
49
+ {"date": "2026-01-06", "dow_prob": 80.0, "sp500_prob": 20.0, "nasdaq_prob": 60.0, "notes": "Momentum test"},
50
+ ]
51
+ }
52
+ }
53
+ path.parent.mkdir(parents=True, exist_ok=True)
54
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
55
+
56
+
57
+ def seed_fixture_history(root: Path) -> None:
58
+ historical_dir = root / "data" / "historical"
59
+ write_csv(
60
+ historical_dir / "DJI_daily.csv",
61
+ [
62
+ {"Date": "12/31/2025", "Open": "0", "High": "0", "Low": "0", "Close": "100"},
63
+ {"Date": "01/02/2026", "Open": "0", "High": "0", "Low": "0", "Close": "101"},
64
+ {"Date": "01/05/2026", "Open": "0", "High": "0", "Low": "0", "Close": "100"},
65
+ {"Date": "01/06/2026", "Open": "0", "High": "0", "Low": "0", "Close": "102"},
66
+ ],
67
+ )
68
+ write_csv(
69
+ historical_dir / "GSPC_daily.csv",
70
+ [
71
+ {"Date": "12/31/2025", "Open": "0", "High": "0", "Low": "0", "Close": "200"},
72
+ {"Date": "01/02/2026", "Open": "0", "High": "0", "Low": "0", "Close": "199"},
73
+ {"Date": "01/05/2026", "Open": "0", "High": "0", "Low": "0", "Close": "200"},
74
+ {"Date": "01/06/2026", "Open": "0", "High": "0", "Low": "0", "Close": "198"},
75
+ ],
76
+ )
77
+ write_csv(
78
+ historical_dir / "IXIC_daily.csv",
79
+ [
80
+ {"Date": "12/31/2025", "Open": "0", "High": "0", "Low": "0", "Close": "300"},
81
+ {"Date": "01/02/2026", "Open": "0", "High": "0", "Low": "0", "Close": "300"},
82
+ {"Date": "01/05/2026", "Open": "0", "High": "0", "Low": "0", "Close": "303"},
83
+ {"Date": "01/06/2026", "Open": "0", "High": "0", "Low": "0", "Close": "300"},
84
+ ],
85
+ )
86
+
87
+
88
+ class TestSeedAccuracyScript(unittest.TestCase):
89
+ def make_project_root(self, name: str) -> Path:
90
+ root = REPO_ROOT / "tmp_feedback_test_main" / name
91
+ shutil.rmtree(root, ignore_errors=True)
92
+ root.mkdir(parents=True, exist_ok=True)
93
+ return root
94
+
95
+ def run_script(self, project_root: Path) -> subprocess.CompletedProcess[str]:
96
+ return subprocess.run(
97
+ [sys.executable, str(SCRIPT_PATH)],
98
+ cwd=project_root,
99
+ capture_output=True,
100
+ text=True,
101
+ check=False,
102
+ )
103
+
104
+ def test_seed_accuracy_generates_output_from_primary_almanac_json(self):
105
+ project_root = self.make_project_root("seed_accuracy_primary")
106
+ try:
107
+ write_primary_almanac(project_root / "data" / "almanac_2026" / "almanac_2026.json")
108
+ seed_fixture_history(project_root)
109
+
110
+ result = self.run_script(project_root)
111
+
112
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
113
+ self.assertIn("January", result.stdout)
114
+ self.assertIn("Q1 Total", result.stdout)
115
+
116
+ output_path = project_root / "data" / "almanac_2026" / "accuracy_results.json"
117
+ payload = json.loads(output_path.read_text(encoding="utf-8"))
118
+
119
+ self.assertEqual(payload["meta"]["total_days_scored"], 3)
120
+ self.assertEqual(payload["meta"]["data_range"]["from"], "2026-01-02")
121
+ self.assertEqual(payload["meta"]["data_range"]["to"], "2026-01-06")
122
+ self.assertEqual(payload["daily"]["2026-01-02"]["hits"], 2)
123
+ self.assertEqual(payload["daily"]["2026-01-02"]["total_calls"], 2)
124
+ self.assertEqual(payload["daily"]["2026-01-02"]["results"]["n"]["verdict"], None)
125
+ self.assertEqual(payload["daily"]["2026-01-06"]["results"]["n"]["verdict"], "MISS")
126
+ self.assertEqual(payload["weekly"]["2026-W00"]["hits"], 2)
127
+ self.assertEqual(payload["weekly"]["2026-W01"]["nasdaq"]["pct"], 50.0)
128
+ self.assertEqual(payload["monthly"]["2026-01"]["hits"], 7)
129
+ self.assertEqual(payload["monthly"]["2026-01"]["total_calls"], 8)
130
+ self.assertEqual(payload["monthly"]["2026-01"]["accuracy"], 87.5)
131
+ self.assertEqual(payload["monthly"]["2026-01"]["trading_days"], 3)
132
+ finally:
133
+ shutil.rmtree(project_root, ignore_errors=True)
134
+
135
+ def test_seed_accuracy_falls_back_to_structured_dump(self):
136
+ project_root = self.make_project_root("seed_accuracy_fallback")
137
+ try:
138
+ write_fallback_almanac(project_root / "data" / "almanac_2026" / "almanac_2026_db_dump.json")
139
+ seed_fixture_history(project_root)
140
+
141
+ result = self.run_script(project_root)
142
+
143
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
144
+ output_path = project_root / "data" / "almanac_2026" / "accuracy_results.json"
145
+ payload = json.loads(output_path.read_text(encoding="utf-8"))
146
+ self.assertEqual(payload["daily"]["2026-01-05"]["hits"], 3)
147
+ self.assertEqual(payload["daily"]["2026-01-06"]["context"], "Momentum test")
148
+ finally:
149
+ shutil.rmtree(project_root, ignore_errors=True)
150
+
151
+ def test_seed_accuracy_returns_error_when_history_is_missing(self):
152
+ project_root = self.make_project_root("seed_accuracy_missing_history")
153
+ try:
154
+ write_primary_almanac(project_root / "data" / "almanac_2026" / "almanac_2026.json")
155
+
156
+ result = self.run_script(project_root)
157
+
158
+ self.assertEqual(result.returncode, 1)
159
+ self.assertIn("Missing historical CSV", result.stderr)
160
+ finally:
161
+ shutil.rmtree(project_root, ignore_errors=True)
162
+
163
+
164
+ if __name__ == "__main__":
165
+ unittest.main()