Spaces:
Running
Running
Commit ·
d7022e8
1
Parent(s): d54c280
Add Almanac accuracy seed inputs
Browse files- data/historical/DJI_daily.csv +67 -0
- data/historical/GSPC_daily.csv +67 -0
- data/historical/IXIC_daily.csv +67 -0
- scripts/seed_accuracy.py +412 -0
- tests/test_seed_accuracy.py +165 -0
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()
|