Jitendra12421 commited on
Commit
4ea71b3
·
verified ·
1 Parent(s): 22a0b79

Upload 46 files

Browse files
.gitattributes CHANGED
@@ -38,3 +38,4 @@ nifty_backend/__pycache__/runtime.cpython-311.pyc filter=lfs diff=lfs merge=lfs
38
  backend/models/yahoo_history_cache.sqlite3 filter=lfs diff=lfs merge=lfs -text
39
  backend/nifty_backend/__pycache__/runtime.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
40
  models/nifty_forecaster/__pycache__/train.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
 
 
38
  backend/models/yahoo_history_cache.sqlite3 filter=lfs diff=lfs merge=lfs -text
39
  backend/nifty_backend/__pycache__/runtime.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
40
  models/nifty_forecaster/__pycache__/train.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
41
+ nifty_backend/__pycache__/runtime.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
models/nifty_opening_mfe_regressor/outputs/latest_prediction.bak ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ input_date,first5_start,first5_end,first5_close,predicted_up_points,predicted_down_points
2
+ 2026-06-11,2026-06-11 09:15:00,2026-06-11 09:19:00,23112.650390625,80.59007717781284,76.48054546871595
models/nifty_opening_mfe_regressor/outputs/latest_prediction.csv CHANGED
@@ -1,2 +1,2 @@
1
  input_date,first5_start,first5_end,first5_close,predicted_up_points,predicted_down_points
2
- 2026-06-09,2026-06-09 09:15:00,2026-06-09 09:19:00,23234.849609375,80.52086115732924,74.45651902291536
 
1
  input_date,first5_start,first5_end,first5_close,predicted_up_points,predicted_down_points
2
+ 2026-06-11,2026-06-11 09:15:00,2026-06-11 09:19:00,23112.650390625,80.59007717781284,76.48054546871592
models/nifty_opening_mfe_regressor/outputs/summary.json CHANGED
@@ -1,47 +1 @@
1
- {
2
- "target_definition": "Predict remaining same-day NIFTY upside/downside points after the first five 1-minute bars.",
3
- "train_rows": 2221,
4
- "valid_rows": 405,
5
- "test_rows": 199,
6
- "train_start": "2015-01-09",
7
- "train_end": "2023-12-29",
8
- "valid_start": "2024-01-01",
9
- "valid_end": "2025-08-14",
10
- "test_start": "2025-08-18",
11
- "test_end": "2026-06-09",
12
- "feature_count": 188,
13
- "up": {
14
- "target": "after5_up_points",
15
- "selected_model": "random_forest_d6_l10_all+affine_s1.26_b-4",
16
- "selected_feature_count": 188,
17
- "validation_mae_points": 62.814071097341994,
18
- "validation_rmse_points": 95.96458512713473,
19
- "test_mae_points": 55.869403284612446,
20
- "test_rmse_points": 79.459874253419,
21
- "test_high_mfe_mae_points": 120.36235726321215,
22
- "test_low_mfe_mae_points": 34.22747241931052,
23
- "baseline_test_mae_points": 75.35728446922032,
24
- "test_mae_improvement_pct": 25.860646813205825,
25
- "latest_prediction_points": 63.15162391627527
26
- },
27
- "down": {
28
- "target": "after5_down_points",
29
- "selected_model": "random_forest_d7_l10_150",
30
- "selected_feature_count": 150,
31
- "validation_mae_points": 72.01385673724953,
32
- "validation_rmse_points": 122.53041341022657,
33
- "test_mae_points": 62.985231430060715,
34
- "test_rmse_points": 97.51356947384825,
35
- "test_high_mfe_mae_points": 141.1492761242954,
36
- "test_low_mfe_mae_points": 36.75568623065309,
37
- "baseline_test_mae_points": 77.89575416143268,
38
- "test_mae_improvement_pct": 19.141637297035604,
39
- "latest_prediction_points": 71.5788486914341
40
- },
41
- "latest_input_date": "2026-06-09",
42
- "latest_first5_start": "2026-06-09 09:15:00",
43
- "latest_first5_end": "2026-06-09 09:19:00",
44
- "latest_first5_close": 23234.849609375,
45
- "latest_predicted_up_points": 63.15162391627527,
46
- "latest_predicted_down_points": 71.5788486914341
47
- }
 
1
+ {"target_definition": "Predict remaining same-day NIFTY upside/downside points after the first five 1-minute bars.", "train_rows": 2221, "valid_rows": 405, "test_rows": 199, "train_start": "2015-01-09", "train_end": "2023-12-29", "valid_start": "2024-01-01", "valid_end": "2025-08-14", "test_start": "2025-08-18", "test_end": "2026-06-09", "feature_count": 188, "up": {"target": "after5_up_points", "selected_model": "random_forest_d6_l10_all+affine_s1.26_b-4", "selected_feature_count": 188, "validation_mae_points": 62.814071097341994, "validation_rmse_points": 95.96458512713473, "test_mae_points": 55.869403284612446, "test_rmse_points": 79.459874253419, "test_high_mfe_mae_points": 120.36235726321215, "test_low_mfe_mae_points": 34.22747241931052, "baseline_test_mae_points": 75.35728446922032, "test_mae_improvement_pct": 25.860646813205825, "latest_prediction_points": 63.15162391627527}, "down": {"target": "after5_down_points", "selected_model": "random_forest_d7_l10_150", "selected_feature_count": 150, "validation_mae_points": 72.01385673724953, "validation_rmse_points": 122.53041341022657, "test_mae_points": 62.985231430060715, "test_rmse_points": 97.51356947384825, "test_high_mfe_mae_points": 141.1492761242954, "test_low_mfe_mae_points": 36.75568623065309, "baseline_test_mae_points": 77.89575416143268, "test_mae_improvement_pct": 19.141637297035604, "latest_prediction_points": 71.5788486914341}, "latest_first5_start": "2026-06-09 09:15:00", "latest_first5_end": "2026-06-09 09:19:00", "latest_first5_close": 23234.849609375}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/nifty_opening_mfe_regressor/outputs/test_predictions.bak ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ date,first5_close,day_high,day_low,day_close,after5_up_points,after5_down_points,predicted_up_points,predicted_down_points
2
+ 2025-08-18,24943.3,25022.0,24852.85,24884.05,78.70000000000073,90.45000000000071,70.74537556748926,69.95608867077122
3
+ 2025-08-19,24905.95,25012.65,24873.95,24989.7,106.70000000000071,32.0,69.58622767243469,72.86841515213641
4
+ 2025-08-20,24943.7,25088.7,24929.7,25047.15,145.0,14.0,72.2215386978149,66.87535067178419
5
+ 2025-08-21,25074.4,25153.65,25054.9,25076.95,79.25,19.5,75.9678756477438,85.01325608135507
6
+ 2025-08-22,25022.5,25084.85,24859.15,24869.45,62.34999999999855,163.34999999999854,76.00333285776364,74.38421945646837
7
+ 2025-08-25,24923.8,25021.55,24894.35,24978.55,97.75,29.450000000000728,72.08624856357176,72.36965738462226
8
+ 2025-08-26,24849.75,24919.65,24689.6,24710.7,69.90000000000146,160.15000000000146,81.82237433198587,75.67181816799939
9
+ 2025-08-28,24577.25,24702.65,24481.6,24533.1,125.40000000000146,95.65000000000146,107.37349857848692,103.48101375434892
10
+ 2025-08-29,24507.4,24572.45,24404.7,24433.65,65.04999999999927,102.70000000000071,92.69929951194534,127.36320204934307
11
+ 2025-09-01,24523.5,24635.6,24432.7,24624.3,112.09999999999854,90.79999999999929,85.59277839975796,102.11100920697749
12
+ 2025-09-02,24662.9,24756.1,24522.35,24575.0,93.19999999999708,140.5500000000029,71.68120758636995,76.0402487590437
13
+ 2025-09-03,24541.0,24737.05,24533.2,24713.6,196.04999999999927,7.799999999999272,97.43911992220336,85.04967811472694
14
+ 2025-09-04,24863.8,24980.75,24708.2,24739.8,116.95000000000071,155.59999999999854,103.12730476704785,78.17111502420333
15
+ 2025-09-05,24827.5,24832.35,24621.6,24743.95,4.849999999998545,205.90000000000143,73.98230098745448,85.62699852482866
16
+ 2025-09-08,24787.65,24885.5,24751.55,24791.2,97.84999999999854,36.10000000000218,72.29944694258762,65.75039472494996
17
+ 2025-09-09,24854.2,24891.8,24814.0,24878.8,37.59999999999855,40.20000000000073,69.44370753005204,77.52099482542334
18
+ 2025-09-10,24962.65,25035.7,24915.05,24977.55,73.04999999999927,47.60000000000218,71.89450651813125,72.72213315583208
19
+ 2025-09-11,25000.8,25037.3,24940.15,25008.1,36.5,60.64999999999782,70.68275488183907,73.77386846663569
20
+ 2025-09-12,25053.3,25139.45,25038.05,25107.7,86.15000000000146,15.25,72.04701987775606,72.71521942996507
21
+ 2025-09-15,25091.4,25138.45,25048.75,25069.7,47.04999999999927,42.650000000001455,74.37466301106662,56.21764396713863
22
+ 2025-09-16,25106.75,25261.4,25070.45,25254.45,154.65000000000146,36.29999999999927,70.4334423975961,77.09468335603322
23
+ 2025-09-17,25297.05,25346.5,25275.35,25330.15,49.45000000000073,21.700000000000728,70.23690402772418,73.54908914096771
24
+ 2025-09-18,25411.2,25448.95,25329.75,25420.75,37.75,81.45000000000073,72.10814928193922,68.07462808633801
25
+ 2025-09-19,25381.45,25428.75,25286.3,25352.5,47.29999999999927,95.15000000000146,74.70715216901579,89.46626946473036
26
+ 2025-09-22,25258.55,25331.7,25151.05,25200.2,73.15000000000146,107.5,71.56191508125039,77.53793666571973
27
+ 2025-09-23,25228.05,25261.9,25084.65,25185.8,33.85000000000218,143.39999999999782,69.2148241834104,84.93267760178357
28
+ 2025-09-24,25092.3,25149.85,25027.45,25060.9,57.54999999999927,64.84999999999854,73.87437120520443,78.18656010430726
29
+ 2025-09-25,25054.4,25092.7,24878.3,24904.55,38.29999999999927,176.10000000000218,71.65598616236045,87.4073952684888
30
+ 2025-09-26,24816.0,24868.6,24629.45,24673.1,52.59999999999855,186.54999999999927,75.98342218786617,112.06060895062171
31
+ 2025-09-29,24673.7,24791.3,24606.2,24677.55,117.59999999999854,67.5,83.37538480505997,95.65712047233475
32
+ 2025-09-30,24713.65,24731.8,24587.7,24633.6,18.149999999997817,125.95000000000071,74.9517147798891,87.6251433757387
33
+ 2025-10-01,24620.5,24867.95,24605.95,24853.4,247.4500000000008,14.549999999999272,77.12668758054085,85.91485675791243
34
+ 2025-10-03,24785.05,24904.8,24747.55,24895.0,119.75,37.5,81.10550551698013,78.1100418959116
35
+ 2025-10-06,24927.05,25095.95,24881.65,25072.55,168.90000000000146,45.39999999999782,69.46213473733515,68.8959058147205
36
+ 2025-10-07,25084.9,25220.9,25076.3,25112.8,136.0,8.600000000002183,78.44188298577762,71.62453401261332
37
+ 2025-10-08,25146.05,25192.5,25008.5,25023.8,46.45000000000073,137.54999999999927,70.87032840015426,81.93097812216612
38
+ 2025-10-09,25058.95,25199.25,25024.3,25170.3,140.29999999999927,34.650000000001455,69.72385100925113,89.36013847299986
39
+ 2025-10-10,25221.2,25330.75,25156.85,25278.2,109.54999999999929,64.35000000000218,73.60123392651,77.07921283623484
40
+ 2025-10-13,25213.8,25267.3,25152.3,25237.15,53.5,61.5,72.9387780959113,71.96467174317387
41
+ 2025-10-14,25293.3,25310.35,25060.55,25123.35,17.049999999999272,232.75,69.75624962391593,75.72942009497733
42
+ 2025-10-15,25239.95,25365.15,25159.35,25327.75,125.20000000000071,80.60000000000218,79.00736543634586,89.70179097400522
43
+ 2025-10-16,25412.15,25625.4,25376.85,25566.3,213.25,35.30000000000291,69.37806626638448,75.22834036609319
44
+ 2025-10-17,25554.05,25781.5,25508.6,25704.7,227.4500000000008,45.45000000000073,70.6399534752275,82.59735724680043
45
+ 2025-10-20,25902.8,25926.2,25788.5,25850.7,23.40000000000145,114.29999999999929,82.0103286656743,71.42501744995688
46
+ 2025-10-21,25903.25,25934.35,25826.3,25833.3,31.099999999998545,76.95000000000073,70.4734596248554,72.82024209764482
47
+ 2025-10-23,26003.75,26104.2,25862.45,25870.3,100.45000000000071,141.29999999999927,78.25616002674558,75.58880300627324
48
+ 2025-10-24,25850.8,25944.15,25718.2,25797.45,93.35000000000218,132.59999999999854,85.9352895781675,84.67147328741909
49
+ 2025-10-27,25861.4,26005.95,25827.0,25974.0,144.54999999999927,34.400000000001455,49.17301873308348,61.10766648725366
50
+ 2025-10-28,25968.1,26041.7,25810.05,25965.4,73.60000000000218,158.04999999999927,72.5812143633312,85.61675283659459
51
+ 2025-10-29,25979.75,26097.85,25960.3,26068.3,118.09999999999854,19.450000000000728,75.08031314406708,79.68242908469409
52
+ 2025-10-30,25985.15,26032.05,25845.25,25891.2,46.89999999999782,139.90000000000146,73.39380876964638,74.325383761321
53
+ 2025-10-31,25862.85,25953.75,25711.2,25732.55,90.90000000000146,151.64999999999782,73.94864338763755,79.1021674617272
54
+ 2025-11-03,25687.85,25803.1,25645.5,25774.3,115.25,42.34999999999855,48.84044727691018,67.28257199135699
55
+ 2025-11-04,25755.4,25787.4,25578.4,25586.25,32.0,177.0,71.3743139533095,89.3481792230919
56
+ 2025-11-06,25666.7,25679.15,25491.55,25519.95,12.450000000000728,175.15000000000146,77.85466759246198,90.57445591555329
57
+ 2025-11-07,25378.15,25551.25,25318.45,25510.05,173.09999999999854,59.70000000000073,82.87633192312792,90.85600003629604
58
+ 2025-11-10,25554.6,25653.45,25503.5,25574.25,98.85000000000218,51.09999999999855,76.07720514892765,92.5203310121857
59
+ 2025-11-11,25544.8,25715.8,25449.25,25705.55,171.0,95.54999999999929,84.99031835536154,91.46776795888675
60
+ 2025-11-12,25807.2,25934.55,25781.15,25874.0,127.34999999999854,26.049999999999272,72.59276739431719,69.43894779255301
61
+ 2025-11-13,25843.95,26010.7,25808.4,25884.1,166.75,35.54999999999927,78.67378928776296,67.05446226149067
62
+ 2025-11-14,25810.7,25940.2,25740.8,25916.8,129.5,69.90000000000146,73.19872933440267,87.55904589996446
63
+ 2025-11-17,25951.05,26024.2,25906.35,26014.3,73.15000000000146,44.70000000000073,69.76315356672858,97.59555559580076
64
+ 2025-11-18,25947.35,26029.85,25876.5,25894.7,82.5,70.84999999999854,85.51147346645418,80.55471837004212
65
+ 2025-11-19,25857.5,26074.65,25856.2,26052.7,217.15000000000143,1.2999999999992724,80.68382090556915,94.6026325119518
66
+ 2025-11-20,26099.7,26246.65,26063.2,26197.4,146.95000000000073,36.5,70.58602673534355,77.26577489877992
67
+ 2025-11-21,26138.75,26179.2,26052.2,26063.95,40.45000000000073,86.54999999999927,72.78494759917074,88.50537506994732
68
+ 2025-11-24,26097.25,26142.8,25912.15,25943.35,45.54999999999927,185.0999999999985,71.59439136628663,98.01259127172162
69
+ 2025-11-25,25965.0,26032.6,25857.5,25860.3,67.59999999999854,107.5,74.81866080624437,97.64266493121366
70
+ 2025-11-26,25969.7,26215.15,25842.95,26203.5,245.4500000000008,126.75,90.3814582306311,86.97774096205734
71
+ 2025-11-27,26251.95,26310.45,26141.9,26219.85,58.5,110.04999999999929,71.40800619164884,75.89199618666595
72
+ 2025-11-28,26242.15,26280.75,26172.4,26204.55,38.59999999999855,69.75,72.7899189230503,68.94343157609912
73
+ 2025-12-01,26285.3,26325.8,26124.2,26175.95,40.5,161.09999999999854,70.47431107358459,75.68478873086691
74
+ 2025-12-02,26141.5,26154.6,25997.85,26057.0,13.099999999998545,143.65000000000146,74.4344525604593,86.98101762857138
75
+ 2025-12-03,26031.25,26066.45,25891.0,25985.1,35.20000000000073,140.25,70.95415206130762,80.43735765423229
76
+ 2025-12-04,25949.55,26098.25,25938.95,26017.1,148.70000000000073,10.599999999998545,70.73708673203622,80.01080718413094
77
+ 2025-12-05,26051.05,26202.6,25985.35,26176.65,151.54999999999927,65.70000000000073,72.41683194442193,96.03195181892916
78
+ 2025-12-08,26159.7,26178.7,25892.25,25932.8,19.0,267.4500000000007,69.67299117722322,81.02444831648698
79
+ 2025-12-09,25819.8,25923.65,25728.0,25841.75,103.85000000000218,91.79999999999929,77.47469251817466,95.02942758445396
80
+ 2025-12-10,25880.55,25947.65,25734.55,25742.65,67.10000000000218,146.0,71.70553491482723,98.1837081927547
81
+ 2025-12-11,25757.1,25922.8,25693.25,25898.4,165.70000000000073,63.84999999999855,76.56354511911341,101.94574105996686
82
+ 2025-12-12,26002.55,26057.6,25938.45,26043.0,55.04999999999927,64.09999999999854,69.67619298888475,79.50154765698446
83
+ 2025-12-15,25953.3,26047.15,25904.75,26014.0,93.85000000000218,48.54999999999927,75.17828185471886,76.85009428534134
84
+ 2025-12-16,25943.3,25980.75,25834.35,25851.35,37.45000000000073,108.95000000000071,71.27112421993293,81.85242825234664
85
+ 2025-12-17,25872.6,25929.15,25770.35,25821.8,56.55000000000291,102.25,72.35835339450391,87.43541006469988
86
+ 2025-12-18,25777.75,25902.35,25726.3,25815.65,124.59999999999854,51.45000000000073,74.43183502860306,85.33298803694375
87
+ 2025-12-19,25925.1,25993.35,25880.45,25961.4,68.25,44.64999999999782,70.77069567565373,83.78604666129061
88
+ 2025-12-22,26093.9,26180.7,26047.8,26162.75,86.79999999999927,46.10000000000218,70.42228403925822,72.76339644091539
89
+ 2025-12-23,26166.7,26233.55,26119.05,26165.95,66.84999999999854,47.650000000001455,74.84971589574519,69.426936900949
90
+ 2025-12-24,26198.25,26236.4,26123.0,26141.65,38.150000000001455,75.25,71.48754603427759,73.72186108153664
91
+ 2025-12-26,26131.05,26144.2,26008.6,26047.65,13.150000000001455,122.45000000000071,45.48128688932305,57.062560579537895
92
+ 2025-12-29,26050.5,26106.8,25920.3,25949.8,56.29999999999927,130.20000000000073,75.70015606822022,78.00564702749449
93
+ 2025-12-30,25908.05,25976.75,25878.0,25970.55,68.70000000000073,30.049999999999272,75.2522086126314,77.13518357533343
94
+ 2025-12-31,26013.65,26187.95,25969.0,26141.85,174.29999999999927,44.650000000001455,73.24189431613242,73.79354269787787
95
+ 2026-01-01,26167.8,26197.55,26113.4,26140.25,29.75,54.39999999999782,71.71010503029348,73.54519160205173
96
+ 2026-01-02,26181.65,26340.0,26118.4,26335.7,158.34999999999854,63.25,45.480429907888656,57.12647950293099
97
+ 2026-01-05,26340.25,26373.2,26210.05,26244.65,32.95000000000073,130.20000000000073,70.46179801639443,81.45276466292157
98
+ 2026-01-06,26195.8,26273.95,26124.75,26174.65,78.15000000000146,71.04999999999927,79.36914045104106,92.38591642666192
99
+ 2026-01-07,26125.65,26187.15,26067.9,26142.9,61.5,57.75,72.55581417361594,83.15694141096833
100
+ 2026-01-08,26123.25,26133.2,25858.45,25868.9,9.950000000000728,264.7999999999993,71.04783101366327,82.46291134118252
101
+ 2026-01-09,25938.8,25940.6,25623.0,25703.7,1.7999999999992724,315.7999999999993,86.53974081668476,121.44759821661056
102
+ 2026-01-12,25611.95,25813.15,25473.4,25806.1,201.2000000000008,138.54999999999927,83.95949739270267,120.28936873981168
103
+ 2026-01-13,25846.25,25899.8,25603.3,25714.2,53.54999999999927,242.9500000000008,78.19680117941645,84.9829292741713
104
+ 2026-01-14,25683.25,25791.75,25603.95,25669.1,108.5,79.29999999999927,82.35604656222411,99.82529425771892
105
+ 2026-01-16,25730.45,25873.5,25662.4,25701.9,143.04999999999927,68.04999999999927,72.2288158291699,96.07795888190776
106
+ 2026-01-19,25597.4,25653.3,25494.35,25555.5,55.89999999999782,103.05000000000292,93.52077464705306,93.8292063036645
107
+ 2026-01-20,25558.25,25585.0,25171.35,25225.35,26.75,386.9000000000015,76.71942144430174,79.78554817218038
108
+ 2026-01-21,25242.4,25300.95,24919.8,25168.1,58.54999999999927,322.6000000000022,130.94997110273525,147.75173361890398
109
+ 2026-01-22,25318.0,25435.75,25168.5,25320.45,117.75,149.5,85.07263391734676,130.19406357107007
110
+ 2026-01-23,25275.3,25347.95,25025.3,25064.75,72.65000000000146,250.0,86.95616205286174,98.93004959582946
111
+ 2026-01-27,25009.95,25246.65,24932.55,25234.1,236.7000000000008,77.40000000000146,110.68260525851476,129.7114495843547
112
+ 2026-01-28,25336.3,25372.1,25187.65,25348.05,35.79999999999927,148.64999999999782,89.71179240489799,109.83708610878163
113
+ 2026-01-29,25280.55,25458.15,25159.8,25422.1,177.60000000000218,120.75,91.59350021026472,87.43822382386271
114
+ 2026-01-30,25259.3,25370.7,25213.65,25315.8,111.40000000000146,45.64999999999782,79.05064818970524,100.1312421862931
115
+ 2026-02-01,25282.8,25440.9,24571.75,24768.0,158.10000000000218,711.0499999999993,80.05582527903351,87.1593675112166
116
+ 2026-02-02,24806.35,25108.1,24679.4,25079.9,301.75,126.94999999999708,86.6858901745076,81.9244305205442
117
+ 2026-02-03,25809.75,26341.2,25641.3,25714.8,531.4500000000007,168.45000000000073,209.3466476279516,140.6950158750343
118
+ 2026-02-04,25710.7,25818.55,25563.95,25737.5,107.84999999999854,146.75,123.34674640202677,112.15100241628755
119
+ 2026-02-05,25727.35,25757.65,25579.5,25641.6,30.30000000000291,147.84999999999854,78.92667069768866,93.48294833342888
120
+ 2026-02-06,25603.5,25703.95,25491.9,25673.6,100.45000000000071,111.59999999999854,81.63730431628646,108.84971109953882
121
+ 2026-02-09,25815.55,25922.25,25780.9,25863.3,106.70000000000071,34.64999999999782,97.04614278500875,83.38428889831818
122
+ 2026-02-10,25918.05,25989.45,25870.45,25916.8,71.40000000000146,47.59999999999855,72.55379470719815,80.47042331164333
123
+ 2026-02-11,25985.0,26009.4,25899.8,25945.35,24.40000000000145,85.20000000000073,70.82761961162062,78.822407881009
124
+ 2026-02-12,25855.25,25906.7,25752.4,25796.15,51.45000000000073,102.84999999999854,74.42962687752149,69.6320381757916
125
+ 2026-02-13,25596.4,25630.35,25444.3,25460.55,33.94999999999709,152.10000000000218,78.73111917226437,92.65335041306253
126
+ 2026-02-16,25472.2,25697.0,25372.7,25682.45,224.79999999999927,99.5,87.51077339277226,135.77441133340997
127
+ 2026-02-17,25612.45,25764.4,25570.3,25716.0,151.95000000000073,42.150000000001455,74.20532764340571,70.59556735334088
128
+ 2026-02-18,25742.05,25828.05,25645.15,25805.7,86.0,96.89999999999782,69.60174388514015,87.14731231617911
129
+ 2026-02-19,25853.9,25885.3,25388.75,25416.45,31.399999999997817,465.1500000000015,72.82233481769892,77.47993082146262
130
+ 2026-02-20,25452.9,25663.55,25379.75,25565.9,210.64999999999785,73.15000000000146,101.23325383545428,120.39677579437908
131
+ 2026-02-23,25698.35,25771.45,25609.35,25704.0,73.10000000000218,89.0,78.13994225569839,102.6805412273098
132
+ 2026-02-24,25570.55,25641.8,25327.6,25460.25,71.25,242.9500000000008,84.66998953575761,78.35520991871599
133
+ 2026-02-25,25561.8,25652.6,25428.2,25478.65,90.79999999999929,133.59999999999854,78.48076576983638,96.14961930214676
134
+ 2026-02-26,25535.1,25572.95,25400.95,25493.3,37.85000000000218,134.14999999999782,73.42761359209224,92.16432309346206
135
+ 2026-02-27,25395.4,25476.4,25141.3,25181.8,81.0,254.10000000000213,84.86928109002694,89.3313420570492
136
+ 2026-03-02,24892.95,24989.35,24603.5,24849.75,96.39999999999782,289.4500000000007,111.27759797579716,107.15701801512222
137
+ 2026-03-04,24385.1,24602.45,24305.4,24473.95,217.35000000000213,79.69999999999709,133.72126269580576,155.30789682765342
138
+ 2026-03-05,24584.85,24854.2,24529.4,24737.45,269.3500000000022,55.44999999999709,162.08234784984276,162.40605803165704
139
+ 2026-03-06,24594.4,24700.9,24415.75,24469.4,106.5,178.65000000000146,117.98380433944612,116.83602544783804
140
+ 2026-03-09,23774.35,24078.15,23697.8,24005.8,303.8000000000029,76.54999999999927,192.3591586795632,146.88020524223515
141
+ 2026-03-10,24149.0,24303.8,24079.95,24286.15,154.79999999999927,69.04999999999927,192.7937738097792,158.9186209194774
142
+ 2026-03-11,24285.9,24299.0,23834.3,23848.2,13.099999999998545,451.6000000000022,133.54653076939334,121.19414544015564
143
+ 2026-03-12,23563.15,23833.15,23556.3,23639.35,270.0,6.850000000002183,192.6358883055884,120.53074403063889
144
+ 2026-03-13,23450.25,23492.4,23112.0,23170.9,42.150000000001455,338.25,145.7091162926563,146.68104013479658
145
+ 2026-03-16,23172.55,23502.0,22955.25,23355.6,329.4500000000007,217.29999999999927,170.38500881814582,207.8442210393264
146
+ 2026-03-17,23395.55,23656.8,23346.6,23558.8,261.25,48.95000000000073,156.7943098659703,149.21233378254283
147
+ 2026-03-18,23721.9,23862.25,23618.45,23764.1,140.34999999999854,103.45000000000071,121.95109115304878,116.42727703008954
148
+ 2026-03-19,23289.5,23378.7,22930.35,23087.85,89.20000000000073,359.15000000000146,133.90428933906327,137.31867907141643
149
+ 2026-03-20,23281.5,23345.15,23067.6,23134.65,63.650000000001455,213.90000000000143,159.7676630786322,165.0265464253639
150
+ 2026-03-23,22687.45,22851.7,22471.25,22492.65,164.25,216.2000000000008,145.85353407241462,152.4341252582677
151
+ 2026-03-24,22785.15,23057.3,22624.2,22958.4,272.1499999999978,160.95000000000073,207.01095264874107,154.6488830452536
152
+ 2026-03-25,23162.6,23465.35,23063.2,23309.0,302.75,99.39999999999782,132.30015580160267,124.7102392167061
153
+ 2026-03-27,23041.8,23186.1,22804.55,22839.5,144.29999999999927,237.25,138.49642324859562,132.77812683059364
154
+ 2026-03-30,22522.45,22714.1,22283.85,22379.2,191.64999999999785,238.60000000000213,137.43517166951938,113.26298404384518
155
+ 2026-04-01,22887.0,22941.3,22618.6,22703.15,54.29999999999927,268.40000000000146,171.94720651853035,142.51587723198915
156
+ 2026-04-02,22228.0,22782.3,22182.55,22700.7,554.2999999999993,45.45000000000073,157.50885517232763,135.03847736028655
157
+ 2026-04-06,22666.05,22998.35,22542.95,22959.45,332.2999999999993,123.09999999999854,144.97157821899438,110.86736478379764
158
+ 2026-04-07,22773.05,23153.85,22719.3,23129.95,380.7999999999993,53.75,111.3757230804756,120.98351003129396
159
+ 2026-04-08,23899.95,23961.25,23837.65,23892.75,61.29999999999927,62.29999999999927,102.2801592496814,103.1134383545202
160
+ 2026-04-09,23935.650390625,23989.75,23683.349609375,23766.05078125,54.099609375,252.30078125,97.56128839578804,120.80458797190984
161
+ 2026-04-10,23965.69921875,24073.80078125,23867.19921875,24051.80078125,108.1015625,98.5,85.23780890426106,116.17509968142504
162
+ 2026-04-13,23578.55078125,23905.650390625,23556.150390625,23818.900390625,327.099609375,22.400390625,101.5126595166479,98.57767387007569
163
+ 2026-04-15,24230.849609375,24273.150390625,24146.69921875,24211.900390625,42.30078125,84.150390625,87.99325993978478,100.5389864062017
164
+ 2026-04-16,24390.55078125,24400.099609375,24103.099609375,24188.400390625,9.548828125,287.451171875,75.10031206149125,96.78284688386456
165
+ 2026-04-17,24207.55078125,24370.25,24109.44921875,24366.900390625,162.69921875,98.1015625,93.875672724558,113.6195855929118
166
+ 2026-04-20,24373.099609375,24394.05078125,24318.0,24330.900390625,20.951171875,55.099609375,74.03118715657969,70.17966380671794
167
+ 2026-04-21,24468.849609375,24600.849609375,24357.150390625,24581.05078125,132.0,111.69921875,86.20844571936449,102.2667119048698
168
+ 2026-04-22,24479.30078125,24515.75,24353.69921875,24367.650390625,36.44921875,125.6015625,78.71803618671171,105.18692808784276
169
+ 2026-04-23,24201.349609375,24309.900390625,24138.849609375,24156.05078125,108.55078125,62.5,84.05594012758098,99.00734685037855
170
+ 2026-04-24,24124.19921875,24203.349609375,23815.349609375,23903.94921875,79.150390625,308.849609375,90.14180250621698,136.87579336691633
171
+ 2026-04-27,24020.349609375,24130.30078125,23952.0,24110.19921875,109.951171875,68.349609375,99.59614157374158,118.31424726361335
172
+ 2026-04-28,24093.849609375,24181.75,23958.05078125,24016.5,87.900390625,135.798828125,88.15968826749,106.64812371296172
173
+ 2026-04-29,24090.80078125,24334.19921875,24060.650390625,24163.599609375,243.3984375,30.150390625,87.90088049432329,124.95795081910784
174
+ 2026-04-30,23945.25,24086.94921875,23797.05078125,23997.55078125,141.69921875,148.19921875,139.48308205556245,111.61338896287434
175
+ 2026-05-04,24177.05078125,24289.19921875,24005.30078125,24119.30078125,112.1484375,171.75,147.62463980491148,110.82979739572832
176
+ 2026-05-05,24064.19921875,24080.94921875,23883.5,24032.80078125,16.75,180.69921875,104.19136737845044,118.00286130314204
177
+ 2026-05-06,24175.80078125,24355.55078125,23999.0,24330.94921875,179.75,176.80078125,89.34877716071158,100.09239323156596
178
+ 2026-05-07,24318.25,24481.94921875,24284.650390625,24326.650390625,163.69921875,33.599609375,144.18742265088838,101.03766931638432
179
+ 2026-05-08,24219.30078125,24253.44921875,24127.69921875,24176.150390625,34.1484375,91.6015625,113.44501924079982,105.61263750990524
180
+ 2026-05-11,23918.75,23997.0,23801.25,23820.349609375,78.25,117.5,115.09320335959804,89.86773115957116
181
+ 2026-05-12,23736.900390625,23754.150390625,23349.099609375,23430.55078125,17.25,387.80078125,116.96920740241298,142.73344731125428
182
+ 2026-05-13,23405.400390625,23582.80078125,23263.05078125,23428.69921875,177.400390625,142.349609375,175.97531700031948,150.41964833343525
183
+ 2026-05-14,23550.05078125,23776.650390625,23426.849609375,23713.75,226.599609375,123.201171875,172.47993492418186,120.64942388533476
184
+ 2026-05-15,23718.900390625,23838.94921875,23610.80078125,23643.5,120.048828125,108.099609375,116.92088358715652,117.22659612227842
185
+ 2026-05-18,23400.5,23695.400390625,23317.55078125,23644.44921875,294.900390625,82.94921875,122.25057682854032,99.44379095807834
186
+ 2026-05-19,23735.099609375,23782.19921875,23587.25,23606.150390625,47.099609375,147.849609375,89.25678651875887,118.2088244294284
187
+ 2026-05-20,23460.650390625,23690.75,23403.75,23664.349609375,230.099609375,56.900390625,90.6801349163642,107.62981114189007
188
+ 2026-05-21,23766.849609375,23859.150390625,23596.849609375,23654.69921875,92.30078125,170.0,143.97390699531186,119.33033220862224
189
+ 2026-05-22,23693.5,23835.599609375,23675.349609375,23748.849609375,142.099609375,18.150390625,110.3358336575918,101.93356091893207
190
+ 2026-05-25,23967.599609375,24054.400390625,23924.400390625,24049.900390625,86.80078125,43.19921875,94.48529797429144,78.70386364984326
191
+ 2026-05-26,24012.55078125,24089.55078125,23885.44921875,23933.75,77.0,127.1015625,79.73076033164853,83.18524021655392
192
+ 2026-05-27,23926.349609375,23983.0,23858.55078125,23907.150390625,56.650390625,67.798828125,82.86160430091547,111.90618562168372
193
+ 2026-05-29,23963.30078125,23998.69921875,23486.599609375,23547.75,35.3984375,476.701171875,103.19623464475224,101.30160095874687
194
+ 2026-06-01,23633.0,23727.650390625,23358.150390625,23379.19921875,94.650390625,274.849609375,147.97873066706777,108.07717150797622
195
+ 2026-06-02,23283.19921875,23556.599609375,23229.150390625,23520.69921875,273.400390625,54.048828125,104.11710291744272,127.52289995410068
196
+ 2026-06-03,23299.30078125,23459.349609375,23152.150390625,23396.94921875,160.048828125,147.150390625,119.81095584527132,121.31295450722716
197
+ 2026-06-04,23345.900390625,23465.150390625,23249.599609375,23416.55078125,119.25,96.30078125,96.4568581173244,123.15485148279662
198
+ 2026-06-05,23456.150390625,23513.650390625,23282.80078125,23366.69921875,57.5,173.349609375,113.41119571155151,102.86641440643322
199
+ 2026-06-08,23130.55078125,23266.849609375,23071.5,23123.0,136.298828125,59.05078125,71.99673733444993,66.32122469457533
200
+ 2026-06-09,23234.849609375,,,,,,80.52086115732924,74.45651902291536
201
+ 2026-06-11,23112.650390625,,,,,,80.59007717781284,76.48054546871595
models/yahoo_history_cache.sqlite3 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:21c414a1f381f1970bf5c16e017a922f17fe6c4ebcb88a15b67d7a192559a4ec
3
- size 319488
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75c3f55a2adee670c10dbadd1c998d3ce773ed067c8e196ed2c7539ca6f8eb87
3
+ size 397312
nifty_backend/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (257 Bytes). View file
 
nifty_backend/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (246 Bytes). View file
 
nifty_backend/__pycache__/runtime.cpython-311.pyc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eac2a9d465d264f6ebb982a4f2b7fbd61ef4db81c5a3549543b07cb103b8d680
3
+ size 138158
nifty_backend/__pycache__/runtime.cpython-312.pyc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:577bb7a5580d51f89125710300b1fb2c2da89613a7053c0c689494052442b6ef
3
+ size 107926
nifty_backend/__pycache__/yahoo_history_client.cpython-311.pyc ADDED
Binary file (27.8 kB). View file
 
nifty_backend/runtime.py CHANGED
@@ -1,10 +1,10 @@
1
  from __future__ import annotations
2
 
3
- import json
4
- import copy
5
- import os
6
- import sys
7
- import threading
8
  from dataclasses import dataclass
9
  from datetime import date, datetime, time, timedelta
10
  from functools import lru_cache
@@ -12,10 +12,10 @@ from pathlib import Path
12
  from typing import Any
13
  from zoneinfo import ZoneInfo
14
 
15
- import joblib
16
- import numpy as np
17
- import pandas as pd
18
- from nifty_backend.yahoo_history_client import YahooHistoryClient
19
 
20
  try:
21
  import pandas_market_calendars as mcal
@@ -24,16 +24,16 @@ except ImportError: # pragma: no cover - production dependency, local fallback
24
 
25
 
26
  IST = ZoneInfo("Asia/Kolkata")
27
- YAHOO_NIFTY_SYMBOL = "^NSEI"
28
- MARKET_CLOSE = time(15, 30)
29
- FIRST5_READY = time(9, 20)
30
- CLOSE_REFRESH_READY = time(15, 45)
31
- TPLUS1_READY = time(14, 30)
32
- STALE_CHECK_INTERVAL_SECONDS = 5
33
- BACKEND_ROOT = Path(__file__).resolve().parents[1]
34
- DATA_DIR = BACKEND_ROOT / "data"
35
- MODEL_DIR = BACKEND_ROOT / "models"
36
- YAHOO_CACHE_PATH = MODEL_DIR / "yahoo_history_cache.sqlite3"
37
  OPENING_DATASET_PATH = DATA_DIR / "opening_direction_training_dataset.parquet"
38
  NIFTY_1M_PATH = DATA_DIR / "nifty50_1m.parquet"
39
  NIFTY_1D_PATH = DATA_DIR / "nifty50_1d.parquet"
@@ -43,21 +43,29 @@ TEST_PREDICTIONS_PATH = DATA_DIR / "test_predictions.parquet"
43
  TOMORROW_MODEL_PATH = MODEL_DIR / "nifty_tomorrow_direction_model.joblib"
44
  TOMORROW_LATEST_PATH = MODEL_DIR / "tomorrow_latest_prediction.csv"
45
  TOMORROW_SUMMARY_PATH = MODEL_DIR / "tomorrow_summary.json"
46
- TOMORROW_TEST_PREDICTIONS_PATH = DATA_DIR / "tomorrow_test_predictions.parquet"
47
- FORECASTING_PROJECT_ROOT = Path(
48
- os.environ.get(
49
- "FORECASTING_PROJECT_ROOT",
50
- str(BACKEND_ROOT.parent.parent / "forecasting project"),
51
- )
52
- )
53
- DAILY_FORECASTER_OUTPUT_DIR = FORECASTING_PROJECT_ROOT / "Code" / "models" / "nifty_forecaster" / "outputs"
54
- DAILY_FORECASTER_SUMMARY_PATH = DAILY_FORECASTER_OUTPUT_DIR / "forecaster_summary.json"
55
- DAILY_FORECASTER_LATEST_PATH = DAILY_FORECASTER_OUTPUT_DIR / "forecaster_latest.csv"
56
- DAILY_FORECASTER_PREDICTIONS_PATH = DAILY_FORECASTER_OUTPUT_DIR / "forecaster_test_predictions.csv"
57
- TPLUS1_MODEL_PATH = MODEL_DIR / "nifty_1420_tplus1_logistic_model.joblib"
 
 
 
 
 
 
58
  TPLUS1_LATEST_PATH = MODEL_DIR / "tplus1_latest_prediction.csv"
59
  TPLUS1_SUMMARY_PATH = MODEL_DIR / "tplus1_summary.json"
60
  TPLUS1_TEST_PREDICTIONS_PATH = DATA_DIR / "tplus1_test_predictions.parquet"
 
 
61
  REFRESH_STATE_PATH = MODEL_DIR / "refresh_state.json"
62
  REFRESH_WAITING = "waiting_second_payload"
63
  REFRESH_REFRESHING = "refreshing"
@@ -81,8 +89,8 @@ DECISION_OVERLAYS = [
81
  },
82
  ]
83
 
84
- _dashboard_payload_lock = threading.Lock()
85
- _stale_refresh_lock = threading.Lock()
86
 
87
 
88
  def utc_now_iso() -> str:
@@ -179,6 +187,31 @@ def previous_trading_day(start: date) -> date:
179
  return pd.Timestamp(schedule.index[-1]).date()
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  class ProbabilityBlend:
183
  def __init__(self, models: list[Any], weights: np.ndarray):
184
  self.models = models
@@ -278,7 +311,7 @@ def read_training_dataset() -> pd.DataFrame:
278
  return df.sort_values("date").reset_index(drop=True)
279
 
280
 
281
- def normalize_yahoo_frame(df: pd.DataFrame) -> pd.DataFrame:
282
  if df.empty:
283
  return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume"])
284
  if isinstance(df.columns, pd.MultiIndex):
@@ -304,81 +337,81 @@ def normalize_yahoo_frame(df: pd.DataFrame) -> pd.DataFrame:
304
  for src, dst in rename.items():
305
  if src in df.columns and dst not in out.columns:
306
  out[dst] = pd.to_numeric(df[src], errors="coerce")
307
- return out.dropna(subset=["date", "open", "high", "low", "close"]).sort_values("date")
308
-
309
-
310
- @lru_cache(maxsize=1)
311
- def yahoo_history_client() -> YahooHistoryClient:
312
- return YahooHistoryClient(cache_path=YAHOO_CACHE_PATH)
313
-
314
-
315
- def period_start(period: str, *, end: datetime) -> datetime:
316
- text = str(period).strip().lower()
317
- units = {
318
- "d": "days",
319
- "wk": "weeks",
320
- "mo": "months",
321
- "y": "years",
322
- }
323
- for suffix, unit in units.items():
324
- if text.endswith(suffix):
325
- raw_value = text[: -len(suffix)]
326
- if not raw_value.isdigit():
327
- break
328
- value = int(raw_value)
329
- if unit == "days":
330
- return end - timedelta(days=value)
331
- if unit == "weeks":
332
- return end - timedelta(weeks=value)
333
- if unit == "months":
334
- return end - timedelta(days=value * 31)
335
- if unit == "years":
336
- return end - timedelta(days=value * 366)
337
- raise ValueError(f"Unsupported Yahoo period: {period!r}")
338
-
339
-
340
- def yahoo_history_to_ohlcv(frame: pd.DataFrame, *, daily: bool) -> pd.DataFrame:
341
- if frame.empty:
342
- return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume"])
343
- out = frame.rename(columns={"timestamp": "date"}).copy()
344
- out["date"] = pd.to_datetime(out["date"], errors="coerce")
345
- if daily:
346
- out["date"] = out["date"].dt.normalize()
347
- for column in ("open", "high", "low", "close", "volume"):
348
- out[column] = pd.to_numeric(out[column], errors="coerce")
349
- return (
350
- out[["date", "open", "high", "low", "close", "volume"]]
351
- .dropna(subset=["date", "open", "high", "low", "close"])
352
- .drop_duplicates("date", keep="last")
353
- .sort_values("date")
354
- .reset_index(drop=True)
355
- )
356
-
357
-
358
- def fetch_yahoo_minutes(period: str = "5d") -> pd.DataFrame:
359
- end = datetime.now(IST).replace(tzinfo=None) + timedelta(minutes=5)
360
- start = period_start(period, end=end)
361
- raw = yahoo_history_client().fetch_history(
362
- YAHOO_NIFTY_SYMBOL,
363
- interval="1m",
364
- start=start,
365
- end=end,
366
- include_prepost=False,
367
- )
368
- return yahoo_history_to_ohlcv(raw, daily=False)
369
-
370
-
371
- def fetch_yahoo_daily(period: str = "1mo") -> pd.DataFrame:
372
- end = datetime.now(IST).replace(tzinfo=None) + timedelta(days=1)
373
- start = period_start(period, end=end)
374
- raw = yahoo_history_client().fetch_history(
375
- YAHOO_NIFTY_SYMBOL,
376
- interval="1d",
377
- start=start,
378
- end=end,
379
- include_prepost=False,
380
- )
381
- return yahoo_history_to_ohlcv(raw, daily=True)
382
 
383
 
384
  def append_parquet_rows(path: Path, new_rows: pd.DataFrame, subset: list[str]) -> pd.DataFrame:
@@ -396,6 +429,11 @@ def append_parquet_rows(path: Path, new_rows: pd.DataFrame, subset: list[str]) -
396
  return combined
397
 
398
 
 
 
 
 
 
399
  def latest_parquet_date(path: Path) -> date | None:
400
  if not path.exists():
401
  return None
@@ -527,6 +565,15 @@ def predict_row(row: pd.DataFrame) -> Prediction:
527
  is_overridden=is_overridden,
528
  )
529
  pd.DataFrame([prediction.to_dict()]).to_csv(LATEST_PATH, index=False)
 
 
 
 
 
 
 
 
 
530
  return prediction
531
 
532
 
@@ -553,125 +600,239 @@ def latest_saved_prediction() -> dict[str, Any]:
553
  return dict(_latest_saved_prediction_cached(_file_cache_key(LATEST_PATH), _file_cache_key(MODEL_DIR / "summary.json")))
554
 
555
 
556
- def _latest_saved_prediction_uncached() -> dict[str, Any]:
557
  if LATEST_PATH.exists():
558
  return pd.read_csv(LATEST_PATH).iloc[-1].to_dict()
559
  summary_path = MODEL_DIR / "summary.json"
560
  if summary_path.exists():
561
  return json.loads(summary_path.read_text(encoding="utf-8"))
562
- raise FileNotFoundError("No latest prediction is available yet.")
563
-
564
-
565
- def _read_daily_forecaster_summary() -> dict[str, Any] | None:
566
- if not DAILY_FORECASTER_SUMMARY_PATH.exists():
567
- return None
568
- raw = json.loads(DAILY_FORECASTER_SUMMARY_PATH.read_text(encoding="utf-8"))
569
- if isinstance(raw, list):
570
- matches = [row for row in raw if row.get("symbol") == "NIFTY 50"]
571
- summary = dict(matches[0] if matches else raw[0])
572
- elif isinstance(raw, dict):
573
- summary = dict(raw)
574
- else:
575
- return None
576
- config = summary.get("config") if isinstance(summary.get("config"), dict) else {}
577
- summary.setdefault("symbol", "NIFTY 50")
578
- summary.setdefault("horizon", "daily")
579
- summary.setdefault("horizon_bars", 1)
580
- summary["model_name"] = "nifty_tomorrow_direction_model"
581
- summary["source_model"] = str(config.get("name") or summary.get("source_model") or "locked_multiwindow_nifty50_ensemble")
582
- summary["target"] = "next trading session NIFTY 50 direction"
583
- summary["artifact_type"] = "daily_forecaster_outputs"
584
- summary["artifact_source"] = str(DAILY_FORECASTER_OUTPUT_DIR)
585
- return summary
586
-
587
-
588
- def _read_daily_forecaster_latest(summary: dict[str, Any]) -> dict[str, Any] | None:
589
- if not DAILY_FORECASTER_LATEST_PATH.exists():
590
- return None
591
- latest = pd.read_csv(DAILY_FORECASTER_LATEST_PATH)
592
- if latest.empty:
593
- return None
594
- if "symbol" in latest.columns:
595
- filtered = latest[latest["symbol"].astype(str) == "NIFTY 50"]
596
- if not filtered.empty:
597
- latest = filtered
598
- row = {k: (None if pd.isna(v) else v) for k, v in latest.iloc[-1].to_dict().items()}
599
- input_date = row.get("latest_forecast_date") or row.get("input_date")
600
- target_date = row.get("target_date")
601
- if not target_date and input_date:
602
- try:
603
- target_date = next_trading_day(date.fromisoformat(str(input_date)[:10]) + timedelta(days=1)).isoformat()
604
- except Exception:
605
- target_date = None
606
- prob_up = row.get("latest_forecast_prob_up", row.get("prob_up"))
607
- prediction = row.get("latest_forecast_signal", row.get("prediction"))
608
- threshold = row.get("threshold", summary.get("threshold"))
609
- confidence = row.get("confidence")
610
- if confidence is None and prob_up is not None:
611
- try:
612
- confidence = float(max(float(prob_up), 1.0 - float(prob_up)))
613
- except Exception:
614
- confidence = None
615
- return {
616
- "input_date": input_date,
617
- "target_date": target_date,
618
- "prediction": prediction,
619
- "prob_up": prob_up,
620
- "confidence": confidence,
621
- "threshold": threshold,
622
- "model_name": "nifty_tomorrow_direction_model",
623
- "source_model": summary.get("source_model", "locked_multiwindow_nifty50_ensemble"),
624
- "validation_accuracy": summary.get("validation_accuracy"),
625
- "test_accuracy": summary.get("test_accuracy"),
626
- "artifact_source": str(DAILY_FORECASTER_OUTPUT_DIR),
627
- }
628
-
629
-
630
- def sync_daily_forecaster_outputs() -> dict[str, Any] | None:
631
- summary = _read_daily_forecaster_summary()
632
- if summary is None:
633
- return None
634
- latest = _read_daily_forecaster_latest(summary)
635
- TOMORROW_SUMMARY_PATH.write_text(json.dumps(summary, indent=2), encoding="utf-8")
636
- if latest is not None:
637
- pd.DataFrame([latest]).to_csv(TOMORROW_LATEST_PATH, index=False)
638
- if DAILY_FORECASTER_PREDICTIONS_PATH.exists():
639
- predictions = pd.read_csv(DAILY_FORECASTER_PREDICTIONS_PATH)
640
- if "symbol" in predictions.columns:
641
- predictions = predictions[predictions["symbol"].astype(str) == "NIFTY 50"].copy()
642
- if not predictions.empty:
643
- if "pred" in predictions.columns and "prediction" not in predictions.columns:
644
- predictions["prediction"] = np.where(pd.to_numeric(predictions["pred"], errors="coerce") == 1, "UP", "DOWN")
645
- if "correct" not in predictions.columns and {"target", "pred"}.issubset(predictions.columns):
646
- predictions["correct"] = (
647
- pd.to_numeric(predictions["target"], errors="coerce")
648
- == pd.to_numeric(predictions["pred"], errors="coerce")
649
- )
650
- predictions.to_parquet(TOMORROW_TEST_PREDICTIONS_PATH, index=False)
651
- artifact = {
652
- "artifact_type": "daily_forecaster_outputs",
653
- "model_name": "nifty_tomorrow_direction_model",
654
- "source_model": summary.get("source_model", "locked_multiwindow_nifty50_ensemble"),
655
- "threshold": float(summary.get("threshold", 0.54)),
656
- "validation_accuracy": summary.get("validation_accuracy"),
657
- "test_accuracy": summary.get("test_accuracy"),
658
- "validation_prob_std": summary.get("validation_prob_std"),
659
- "test_prob_std": summary.get("test_prob_std"),
660
- "test_prob_min": summary.get("test_prob_min"),
661
- "test_prob_max": summary.get("test_prob_max"),
662
- "artifact_source": str(DAILY_FORECASTER_OUTPUT_DIR),
663
- }
664
- joblib.dump(artifact, TOMORROW_MODEL_PATH)
665
- return latest or summary
666
-
667
-
668
- def load_tomorrow_model_artifact() -> dict[str, Any]:
669
- synced = sync_daily_forecaster_outputs()
670
- if synced is not None and TOMORROW_MODEL_PATH.exists():
671
- return joblib.load(TOMORROW_MODEL_PATH)
672
- if TOMORROW_MODEL_PATH.exists():
673
- return joblib.load(TOMORROW_MODEL_PATH)
674
- summary = load_tomorrow_summary()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  return {
676
  "artifact_type": "daily_forecaster_snapshot",
677
  "model_name": summary.get("model_name", "nifty_tomorrow_direction_model"),
@@ -680,63 +841,63 @@ def load_tomorrow_model_artifact() -> dict[str, Any]:
680
  }
681
 
682
 
683
- def load_tomorrow_summary() -> dict[str, Any]:
684
- synced = sync_daily_forecaster_outputs()
685
- if synced is not None and TOMORROW_SUMMARY_PATH.exists():
686
- return json.loads(TOMORROW_SUMMARY_PATH.read_text(encoding="utf-8"))
687
- if TOMORROW_SUMMARY_PATH.exists():
688
- return json.loads(TOMORROW_SUMMARY_PATH.read_text(encoding="utf-8"))
689
- return {
690
- "model_name": "nifty_tomorrow_direction_model",
691
- "source_model": "locked_multiwindow_nifty50_ensemble",
692
- "target": "next trading session NIFTY 50 direction",
693
- "threshold": 0.54,
694
- "validation_accuracy": 0.5673758865248227,
695
- "test_accuracy": 0.6451612903225806,
696
- "baseline_accuracy": 0.5053763440860215,
697
- "n_test": 186,
698
- "feature_count": 204,
699
- }
700
-
701
-
702
- def latest_tomorrow_prediction() -> dict[str, Any]:
703
- sync_daily_forecaster_outputs()
704
- latest_daily = latest_parquet_date(NIFTY_1D_PATH)
705
- expected_daily = expected_completed_daily_date()
706
- valid_daily = min(latest_daily, expected_daily) if latest_daily and expected_daily else (expected_daily or latest_daily)
707
-
708
- if TOMORROW_LATEST_PATH.exists():
709
- row = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
710
- cleaned = {k: (None if pd.isna(v) else v) for k, v in row.items()}
711
- try:
712
- input_day = date.fromisoformat(str(cleaned.get("input_date"))[:10])
713
- except Exception:
714
- input_day = None
715
- if valid_daily is not None and (input_day is None or input_day < valid_daily):
716
- try:
717
- refreshed = refresh_tomorrow_prediction(session_date=valid_daily)
718
- try:
719
- refreshed_day = date.fromisoformat(str(refreshed.get("input_date"))[:10])
720
- except Exception:
721
- refreshed_day = None
722
- if refreshed_day is not None and refreshed_day >= valid_daily:
723
- return refreshed
724
- except Exception:
725
- pass
726
- return cleaned
727
- summary = load_tomorrow_summary()
728
- try:
729
- summary_input_day = date.fromisoformat(str(summary.get("latest_forecast_date"))[:10])
730
- except Exception:
731
- summary_input_day = None
732
- if valid_daily is not None and (summary_input_day is None or summary_input_day < valid_daily):
733
- try:
734
- return refresh_tomorrow_prediction(session_date=valid_daily)
735
- except Exception:
736
- pass
737
- return {
738
- "input_date": summary.get("latest_forecast_date"),
739
- "target_date": None,
740
  "prediction": summary.get("latest_forecast_signal"),
741
  "prob_up": summary.get("latest_forecast_prob_up"),
742
  "confidence": None,
@@ -892,27 +1053,27 @@ def _apply_tplus1_overlays(pred: np.ndarray, frame: pd.DataFrame, overlays: list
892
  return adjusted
893
 
894
 
895
- def refresh_tplus1_prediction(session_date: date | None = None) -> dict[str, Any]:
896
- if not TPLUS1_MODEL_PATH.exists():
897
- raise FileNotFoundError(f"Missing T+1 model artifact: {TPLUS1_MODEL_PATH}")
898
- payload = joblib.load(TPLUS1_MODEL_PATH)
899
- features = payload["features"]
900
- threshold = float(payload["threshold"])
901
- frame = _add_tplus1_target_features(_build_tplus1_session_features(_minute_frame_for_tplus1()))
902
- if session_date is not None:
903
- row = frame[pd.to_datetime(frame["date"], errors="coerce").dt.date == session_date].tail(1)
904
- else:
905
- row = frame.tail(1)
906
- if row.empty:
907
- minutes = fetch_yahoo_minutes(period="7d")
908
- append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
909
- frame = _add_tplus1_target_features(_build_tplus1_session_features(_minute_frame_for_tplus1()))
910
- if session_date is not None:
911
- row = frame[pd.to_datetime(frame["date"], errors="coerce").dt.date == session_date].tail(1)
912
- else:
913
- row = frame.tail(1)
914
- if row.empty:
915
- raise RuntimeError("No complete 14:00-14:20 window is available for T+1 prediction.")
916
  missing = [col for col in features if col not in row.columns]
917
  if missing:
918
  raise RuntimeError(f"T+1 feature row is missing model features: {missing[:5]}")
@@ -938,8 +1099,10 @@ def refresh_tplus1_prediction(session_date: date | None = None) -> dict[str, Any
938
  "validation_accuracy": summary.get("validation_accuracy"),
939
  "test_accuracy": summary.get("test_accuracy"),
940
  "accuracy_goal": summary.get("accuracy_goal"),
 
941
  }
942
  pd.DataFrame([out]).to_csv(TPLUS1_LATEST_PATH, index=False)
 
943
  clear_dashboard_payload_cache()
944
  return out
945
 
@@ -966,23 +1129,26 @@ def _tomorrow_probability_from_daily(daily: pd.DataFrame, fallback_prob: float)
966
  return float(np.clip(score, 0.35, 0.65))
967
 
968
 
969
- def refresh_tomorrow_prediction(session_date: date | None = None) -> dict[str, Any]:
970
- synced = sync_daily_forecaster_outputs()
971
- if synced is not None and TOMORROW_LATEST_PATH.exists():
972
- latest = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
973
- cleaned = {k: (None if pd.isna(v) else v) for k, v in latest.items()}
974
- if session_date is None:
975
- clear_dashboard_payload_cache()
976
- return cleaned
977
- try:
978
- input_day = date.fromisoformat(str(cleaned.get("input_date"))[:10])
979
- except Exception:
980
- input_day = None
981
- if input_day is not None and (session_date is None or input_day >= session_date):
982
- clear_dashboard_payload_cache()
983
- return cleaned
984
- summary = load_tomorrow_summary()
985
- artifact = load_tomorrow_model_artifact()
 
 
 
986
  daily = pd.read_parquet(NIFTY_1D_PATH)
987
  daily["date"] = pd.to_datetime(daily["date"], errors="coerce").dt.normalize()
988
  daily = daily.dropna(subset=["date"]).sort_values("date")
@@ -1006,21 +1172,23 @@ def refresh_tomorrow_prediction(session_date: date | None = None) -> dict[str, A
1006
  "source_model": str(summary.get("source_model", "tuned_daily_forest_single")),
1007
  "validation_accuracy": float(summary.get("validation_accuracy", 0.5780141843971631)),
1008
  "test_accuracy": float(summary.get("test_accuracy", 0.6182795698924731)),
 
1009
  }
1010
  pd.DataFrame([row]).to_csv(TOMORROW_LATEST_PATH, index=False)
 
1011
  summary = dict(summary)
1012
- summary.update(
1013
- {
1014
- "latest_forecast_date": row["input_date"],
1015
  "latest_forecast_for": f"next trading session {row['target_date']}",
1016
  "latest_forecast_prob_up": row["prob_up"],
1017
  "latest_forecast_signal": row["prediction"],
1018
  "latest_target_date": row["target_date"],
1019
- }
1020
- )
1021
- TOMORROW_SUMMARY_PATH.write_text(json.dumps(summary, indent=2), encoding="utf-8")
1022
- clear_dashboard_payload_cache()
1023
- return row
1024
 
1025
 
1026
  def _json_ready_frame(df: pd.DataFrame, limit: int | None = None) -> list[dict[str, Any]]:
@@ -1034,6 +1202,8 @@ def _json_ready_frame(df: pd.DataFrame, limit: int | None = None) -> list[dict[s
1034
  return out.to_dict(orient="records")
1035
 
1036
 
 
 
1037
  def load_model_summary() -> dict[str, Any]:
1038
  summary_path = MODEL_DIR / "summary.json"
1039
  if not summary_path.exists():
@@ -1096,118 +1266,190 @@ def dashboard_payload() -> dict[str, Any]:
1096
  _file_cache_key(MODEL_DIR / "candidate_results.csv"),
1097
  _file_cache_key(NIFTY_1M_PATH),
1098
  _file_cache_key(LIVE_ACCURACY_PATH),
 
1099
  )
1100
  with _dashboard_payload_lock:
1101
  return copy.deepcopy(_dashboard_payload_cached(key))
1102
 
1103
 
1104
- def warm_dashboard_payload_cache() -> None:
1105
- dashboard_payload()
1106
-
1107
-
1108
- def build_prediction_track_record(
1109
- daily: pd.DataFrame,
1110
- t5_test: pd.DataFrame,
1111
- tomorrow_test: pd.DataFrame,
1112
- tplus1_test: pd.DataFrame,
1113
- t5_latest: dict[str, Any],
1114
- tomorrow_latest: dict[str, Any],
1115
- tplus1_latest: dict[str, Any],
1116
- ) -> list[dict[str, Any]]:
1117
- daily_rows = daily.copy()
1118
- daily_rows["date"] = pd.to_datetime(daily_rows["date"], errors="coerce").dt.normalize()
1119
- daily_rows = daily_rows.dropna(subset=["date"]).sort_values("date")
1120
- daily_rows = daily_rows[
1121
- daily_rows["open"].map(lambda value: np.isfinite(float(value)) if pd.notna(value) else False)
1122
- & daily_rows["close"].map(lambda value: np.isfinite(float(value)) if pd.notna(value) else False)
1123
- ].copy()
1124
- daily_rows = daily_rows[daily_rows["open"].astype(float) != 0]
1125
- completed_day = expected_completed_daily_date()
1126
- daily_rows = daily_rows[daily_rows["date"].dt.date <= completed_day]
1127
- if daily_rows.empty:
1128
- return []
1129
-
1130
- predictions_by_date: dict[str, dict[str, Any]] = {}
1131
-
1132
- def add_prediction(target_date: Any, prediction: Any, source: str, priority: int, meta: dict[str, Any] | None = None) -> None:
1133
- day = str(target_date or "")[:10]
1134
- pred = str(prediction or "").upper()
1135
- if not day or pred not in {"UP", "DOWN"}:
1136
- return
1137
- existing = predictions_by_date.get(day)
1138
- if existing and existing.get("_priority", 0) >= priority:
1139
- return
1140
- predictions_by_date[day] = {
1141
- "prediction": pred,
1142
- "source": source,
1143
- "_priority": priority,
1144
- **(meta or {}),
1145
- }
1146
-
1147
- for _, row in tomorrow_test.iterrows():
1148
- pred = row.get("prediction")
1149
- if pd.isna(pred) and "pred" in row:
1150
- pred = "UP" if int(row.get("pred")) == 1 else "DOWN"
1151
- add_prediction(row.get("target_date") or row.get("date"), pred, "Tomorrow", 20, {"prob_up": row.get("prob_up")})
1152
-
1153
- add_prediction(tomorrow_latest.get("target_date"), tomorrow_latest.get("prediction"), "Tomorrow", 40, {"prob_up": tomorrow_latest.get("prob_up")})
1154
-
1155
- records: list[dict[str, Any]] = []
1156
- for _, row in daily_rows.tail(20).iterrows():
1157
- day = row["date"].date().isoformat()
1158
- day_open = float(row["open"])
1159
- day_close = float(row["close"])
1160
- actual_move = (day_close - day_open) / day_open
1161
- actual_direction = "UP" if actual_move >= 0 else "DOWN"
1162
- pred = predictions_by_date.get(day)
1163
- prediction = pred.get("prediction") if pred else None
1164
- records.append(
1165
- {
1166
- "date": day,
1167
- "prediction": prediction,
1168
- "prediction_source": pred.get("source") if pred else None,
1169
- "prob_up": pred.get("prob_up") if pred else None,
1170
- "actual_move": actual_move,
1171
- "actual_direction": actual_direction,
1172
- "correct": None if prediction is None else prediction == actual_direction,
1173
- }
1174
- )
1175
- return records
1176
-
1177
-
1178
- @lru_cache(maxsize=4)
1179
- def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...]) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1180
  summary = load_model_summary()
1181
  t5_latest = _latest_saved_prediction_uncached()
1182
  tomorrow_summary = load_tomorrow_summary()
1183
  tomorrow_latest = latest_tomorrow_prediction()
1184
  tplus1_summary = load_tplus1_summary()
1185
  tplus1_latest = latest_tplus1_prediction()
1186
- refresh_state = load_refresh_state()
1187
  t5_test = load_test_predictions()
1188
  tomorrow_test = load_tomorrow_test_predictions()
 
1189
  tplus1_test = load_tplus1_test_predictions()
1190
  daily = pd.read_parquet(NIFTY_1D_PATH)
1191
  daily["date"] = pd.to_datetime(daily["date"], errors="coerce")
1192
  daily = daily.sort_values("date").tail(180)
1193
- dataset = read_training_dataset()
1194
- opening = dataset[["date", "first5_return", "first5_range_pct", "first5_close_location"]].tail(120).copy()
1195
 
1196
  if not t5_test.empty:
1197
- recent_predictions = t5_test.tail(40).copy()
1198
- recent_accuracy = float(recent_predictions["correct"].mean())
1199
- direction_mix = t5_test.groupby("prediction")["correct"].agg(["count", "mean"]).reset_index()
1200
- monthly = (
1201
- t5_test.assign(month=t5_test["date"].dt.strftime("%Y-%m"))
1202
- .groupby("month", as_index=False)["correct"]
1203
- .mean()
1204
- .rename(columns={"correct": "accuracy"})
1205
- )
1206
  else:
1207
- recent_predictions = pd.DataFrame()
1208
  recent_accuracy = None
1209
- direction_mix = pd.DataFrame()
1210
- monthly = pd.DataFrame()
1211
 
1212
  if not tomorrow_test.empty:
1213
  tomorrow_recent = tomorrow_test.tail(40).copy()
@@ -1217,7 +1459,6 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
1217
  tomorrow_recent["correct"] = pd.to_numeric(tomorrow_recent["target"], errors="coerce") == pd.to_numeric(tomorrow_recent["pred"], errors="coerce")
1218
  tomorrow_accuracy = float(tomorrow_recent["correct"].mean()) if "correct" in tomorrow_recent.columns else tomorrow_summary.get("test_accuracy")
1219
  else:
1220
- tomorrow_recent = pd.DataFrame()
1221
  tomorrow_accuracy = tomorrow_summary.get("test_accuracy")
1222
 
1223
  model_metrics = [
@@ -1252,59 +1493,46 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
1252
  "test_rows": int(len(t5_test)) if not t5_test.empty else int(summary.get("test_rows") or 0),
1253
  },
1254
  ]
1255
- metrics = {
1256
- "validation_accuracy": tomorrow_summary.get("validation_accuracy"),
1257
- "test_accuracy": tomorrow_summary.get("test_accuracy"),
1258
  "baseline_test_accuracy": tomorrow_summary.get("baseline_accuracy"),
1259
  "validation_auc": summary.get("validation_auc"),
1260
  "test_auc": summary.get("test_auc"),
1261
  "test_brier": summary.get("test_brier"),
1262
  "feature_count": tomorrow_summary.get("feature_count"),
1263
  "recent_accuracy": tomorrow_accuracy,
1264
- "recent_accuracy_days": int(len(tomorrow_recent)) if not tomorrow_recent.empty else 0,
1265
- "total_test_days": int(tomorrow_summary.get("n_test") or len(tomorrow_test) or 0),
1266
- "models": model_metrics,
1267
- }
1268
- track_record = build_prediction_track_record(
1269
- daily,
1270
- t5_test,
1271
- tomorrow_test,
1272
- tplus1_test,
1273
- t5_latest,
1274
- tomorrow_latest,
1275
- tplus1_latest,
1276
- )
1277
- return {
1278
- "latest": t5_latest,
1279
- "tomorrow_latest": tomorrow_latest,
1280
- "tplus1_latest": tplus1_latest,
1281
- "live_accuracy": load_live_accuracy(),
 
 
1282
  "metrics": metrics,
1283
- "summary": summary,
1284
- "tomorrow_summary": tomorrow_summary,
1285
- "tplus1_summary": tplus1_summary,
1286
- "candidates": load_candidate_results(),
1287
  "charts": {
1288
- "daily_close": _json_ready_frame(daily[["date", "open", "high", "low", "close"]]),
1289
- "opening_features": _json_ready_frame(opening),
1290
- "monthly_accuracy": _json_ready_frame(monthly),
1291
- "direction_mix": _json_ready_frame(direction_mix),
1292
- "recent_predictions": _json_ready_frame(recent_predictions),
1293
- "t5_recent_predictions": _json_ready_frame(recent_predictions),
1294
- "tomorrow_recent_predictions": _json_ready_frame(tomorrow_recent),
1295
- "tplus1_recent_predictions": _json_ready_frame(tplus1_test.tail(40)),
1296
- "track_record": track_record,
1297
- },
1298
- "data_status": {
1299
- "nifty_1m_rows": int(len(pd.read_parquet(NIFTY_1M_PATH, columns=["date"]))),
1300
- "nifty_1d_rows": int(len(pd.read_parquet(NIFTY_1D_PATH, columns=["date"]))),
1301
- "training_rows": int(len(dataset)),
1302
- "test_prediction_rows": int(len(t5_test)),
1303
- "tomorrow_test_prediction_rows": int(len(tomorrow_test)),
1304
- "tplus1_test_prediction_rows": int(len(tplus1_test)),
1305
- "latest_daily_date": pd.to_datetime(daily["date"]).max().date().isoformat(),
1306
- "refresh_phase": refresh_state.get("phase", REFRESH_NORMAL),
1307
- "refresh_state": refresh_state,
1308
  },
1309
  }
1310
 
@@ -1323,14 +1551,12 @@ def refresh_first5_prediction(session_date: date | None = None, minutes: pd.Data
1323
  merged = merged.drop_duplicates(subset=["date"], keep="last").sort_values("date").reset_index(drop=True)
1324
  merged.to_parquet(OPENING_DATASET_PATH, index=False, compression="zstd")
1325
  prediction = predict_row(row)
1326
- clear_dashboard_payload_cache()
1327
  return prediction
1328
 
1329
 
1330
  def refresh_daily_data() -> dict[str, Any]:
1331
  daily = fetch_yahoo_daily(period="1mo")
1332
  combined = append_parquet_rows(NIFTY_1D_PATH, daily, ["date"])
1333
- clear_dashboard_payload_cache()
1334
  return {
1335
  "rows": int(len(combined)),
1336
  "latest_date": pd.to_datetime(combined["date"]).max().date().isoformat(),
@@ -1383,7 +1609,6 @@ def update_opening_outcomes_from_daily() -> dict[str, Any]:
1383
  dataset = dataset.drop(columns=["_session_date"])
1384
  dataset = dataset.sort_values("date").reset_index(drop=True)
1385
  dataset.to_parquet(OPENING_DATASET_PATH, index=False, compression="zstd")
1386
- clear_dashboard_payload_cache()
1387
  latest = pd.to_datetime(dataset["date"], errors="coerce").max()
1388
  return {
1389
  "updated_rows": int(updated),
@@ -1391,136 +1616,285 @@ def update_opening_outcomes_from_daily() -> dict[str, Any]:
1391
  }
1392
 
1393
 
1394
- def load_live_accuracy() -> dict[str, Any]:
1395
- """Load the live accuracy ledger from disk."""
1396
- if LIVE_ACCURACY_PATH.exists():
1397
- try:
1398
- return json.loads(LIVE_ACCURACY_PATH.read_text(encoding="utf-8"))
1399
- except Exception:
1400
- pass
1401
- return {
1402
- "tomorrow": {"entries": [], "accuracy": None, "total": 0, "correct_count": 0},
1403
- "t5": {"entries": [], "accuracy": None, "total": 0, "correct_count": 0},
1404
- "tplus1": {"entries": [], "accuracy": None, "total": 0, "correct_count": 0},
1405
- }
1406
-
1407
-
1408
- def save_live_accuracy(data: dict[str, Any]) -> None:
1409
- """Persist the live accuracy ledger to disk."""
1410
- LIVE_ACCURACY_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8")
1411
-
1412
-
1413
- def update_live_accuracy(session_date: date) -> dict[str, Any]:
1414
- """Score today's predictions against actual outcomes and update the ledger.
1415
-
1416
- Must be called AFTER refresh_daily_data() (so today's close is available)
1417
- but BEFORE refresh_first5_prediction / refresh_tplus1_prediction /
1418
- refresh_tomorrow_prediction (so the CSV files still hold the predictions
1419
- we want to score).
1420
- """
1421
- ledger = load_live_accuracy()
1422
- daily = pd.read_parquet(NIFTY_1D_PATH)
1423
- daily["_date"] = pd.to_datetime(daily["date"], errors="coerce").dt.normalize()
1424
- today_rows = daily[daily["_date"].dt.date == session_date]
1425
- if today_rows.empty:
1426
- return ledger
1427
-
1428
- day_open = float(today_rows.iloc[-1]["open"])
1429
- day_close = float(today_rows.iloc[-1]["close"])
1430
- if not (np.isfinite(day_open) and np.isfinite(day_close) and day_open != 0):
1431
- return ledger
1432
- actual_close_gt_open = "UP" if day_close > day_open else "DOWN"
1433
- session_iso = session_date.isoformat()
1434
-
1435
- # --- T+5: today's 9:20 AM prediction vs close > open ---
1436
- logged_t5 = {e["date"] for e in ledger["t5"]["entries"]}
1437
- if session_iso not in logged_t5 and LATEST_PATH.exists():
1438
- try:
1439
- t5_row = pd.read_csv(LATEST_PATH).iloc[-1].to_dict()
1440
- if str(t5_row.get("input_date", ""))[:10] == session_iso:
1441
- pred = str(t5_row.get("prediction", "")).upper()
1442
- if pred in ("UP", "DOWN"):
1443
- ledger["t5"]["entries"].append({
1444
- "date": session_iso,
1445
- "prediction": pred,
1446
- "actual": actual_close_gt_open,
1447
- "correct": pred == actual_close_gt_open,
1448
- })
1449
- except Exception:
1450
- pass
1451
-
1452
- # --- Tomorrow: yesterday's prediction targeting today vs close > open ---
1453
- logged_tom = {e["date"] for e in ledger["tomorrow"]["entries"]}
1454
- if session_iso not in logged_tom and TOMORROW_LATEST_PATH.exists():
1455
- try:
1456
- tom_row = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
1457
- if str(tom_row.get("target_date", ""))[:10] == session_iso:
1458
- pred = str(tom_row.get("prediction", "")).upper()
1459
- if pred in ("UP", "DOWN"):
1460
- ledger["tomorrow"]["entries"].append({
1461
- "date": session_iso,
1462
- "prediction": pred,
1463
- "actual": actual_close_gt_open,
1464
- "correct": pred == actual_close_gt_open,
1465
- })
1466
- except Exception:
1467
- pass
1468
-
1469
- # --- T+1: yesterday's 14:20 prediction targeting today ---
1470
- # T+1 target: today's close > yesterday's 14:20 close
1471
- logged_t1 = {e["date"] for e in ledger["tplus1"]["entries"]}
1472
- if session_iso not in logged_t1 and TPLUS1_LATEST_PATH.exists():
1473
- try:
1474
- t1_row = pd.read_csv(TPLUS1_LATEST_PATH).iloc[-1].to_dict()
1475
- if str(t1_row.get("target_date", ""))[:10] == session_iso:
1476
- pred = str(t1_row.get("prediction", "")).upper()
1477
- input_date_str = str(t1_row.get("input_date", ""))[:10]
1478
- input_day = date.fromisoformat(input_date_str)
1479
- # Read the 14:20 close from minute data for the input session
1480
- minute = pd.read_parquet(NIFTY_1M_PATH, columns=["date", "close"])
1481
- minute["dt"] = pd.to_datetime(minute["date"], errors="coerce")
1482
- minute = minute.dropna(subset=["dt"])
1483
- minute["session_date"] = minute["dt"].dt.normalize()
1484
- minute["time_str"] = minute["dt"].dt.strftime("%H:%M")
1485
- window = minute[
1486
- (minute["session_date"].dt.date == input_day)
1487
- & (minute["time_str"] >= "14:00")
1488
- & (minute["time_str"] <= "14:20")
1489
- ].sort_values("dt")
1490
- if not window.empty and pred in ("UP", "DOWN"):
1491
- w_close = float(window.iloc[-1]["close"])
1492
- t1_actual = "UP" if day_close > w_close else "DOWN"
1493
- ledger["tplus1"]["entries"].append({
1494
- "date": session_iso,
1495
- "prediction": pred,
1496
- "actual": t1_actual,
1497
- "correct": pred == t1_actual,
1498
- })
1499
- except Exception:
1500
- pass
1501
-
1502
- # Recompute summary stats
1503
- for model_id in ("t5", "tomorrow", "tplus1"):
1504
- entries = ledger[model_id]["entries"]
1505
- total = len(entries)
1506
- correct = sum(1 for e in entries if e.get("correct"))
1507
- ledger[model_id]["total"] = total
1508
- ledger[model_id]["correct_count"] = correct
1509
- ledger[model_id]["accuracy"] = correct / total if total > 0 else None
1510
-
1511
- save_live_accuracy(ledger)
1512
- clear_dashboard_payload_cache()
1513
- return ledger
1514
-
1515
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1516
  def refresh_market_close_data(session_date: date | None = None) -> dict[str, Any]:
1517
  now = datetime.now(IST)
1518
  session_date = session_date or now.date()
1519
  if not is_trading_day(session_date):
1520
  raise RuntimeError(f"{session_date.isoformat()} is not an NSE trading session.")
1521
- save_refresh_state(REFRESH_WAITING, session_date=session_date)
1522
  try:
1523
- save_refresh_state(REFRESH_REFRESHING, session_date=session_date)
1524
  minutes = fetch_yahoo_minutes(period="7d")
1525
  minute_frame = append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
1526
  daily_info = refresh_daily_data()
@@ -1533,8 +1907,6 @@ def refresh_market_close_data(session_date: date | None = None) -> dict[str, Any
1533
  tplus1_prediction = refresh_tplus1_prediction(session_date=session_date)
1534
  outcomes = update_opening_outcomes_from_daily()
1535
  tomorrow_prediction = refresh_tomorrow_prediction(session_date=session_date)
1536
- state = save_refresh_state(REFRESH_READY, session_date=session_date)
1537
- clear_dashboard_payload_cache()
1538
  return {
1539
  "session_date": session_date.isoformat(),
1540
  "nifty_1m_rows": int(len(minute_frame)),
@@ -1544,15 +1916,12 @@ def refresh_market_close_data(session_date: date | None = None) -> dict[str, Any
1544
  "t5_prediction": t5_prediction.to_dict(),
1545
  "tplus1_prediction": tplus1_prediction,
1546
  "tomorrow_prediction": tomorrow_prediction,
1547
- "refresh_state": state,
1548
  }
1549
- except Exception as exc:
1550
- save_refresh_state(REFRESH_FAILED, session_date=session_date, error=str(exc))
1551
- clear_dashboard_payload_cache()
1552
  raise
1553
 
1554
 
1555
- def close_refresh_due(now: datetime | None = None) -> bool:
1556
  now = now or datetime.now(IST)
1557
  if not is_trading_day(now.date()) or now.time() < CLOSE_REFRESH_READY:
1558
  return False
@@ -1567,137 +1936,140 @@ def close_refresh_due(now: datetime | None = None) -> bool:
1567
  tomorrow_input = date.fromisoformat(str(tomorrow_latest.get("input_date"))[:10])
1568
  except Exception:
1569
  tomorrow_input = None
1570
- return any(
1571
- latest != now.date()
1572
- for latest in (latest_daily, latest_minutes, latest_opening, latest_opening_outcome, tomorrow_input)
1573
- )
1574
-
1575
-
1576
- def latest_prediction_input_date(path: Path) -> date | None:
1577
- if not path.exists():
1578
- return None
1579
- try:
1580
- frame = pd.read_csv(path, usecols=["input_date"])
1581
- except Exception:
1582
- return None
1583
- if frame.empty:
1584
- return None
1585
- value = pd.to_datetime(frame["input_date"], errors="coerce").max()
1586
- return None if pd.isna(value) else value.date()
1587
-
1588
-
1589
- def latest_tomorrow_input_date() -> date | None:
1590
- try:
1591
- latest = latest_tomorrow_prediction()
1592
- raw = latest.get("input_date")
1593
- return date.fromisoformat(str(raw)[:10]) if raw else None
1594
- except Exception:
1595
- return None
1596
-
1597
-
1598
- def expected_completed_daily_date(now: datetime | None = None) -> date:
1599
- now = now or datetime.now(IST)
1600
- if is_trading_day(now.date()) and now.time() < CLOSE_REFRESH_READY:
1601
- return previous_trading_day(now.date() - timedelta(days=1))
1602
- return previous_trading_day(now.date())
1603
-
1604
-
1605
- def expected_minute_date(now: datetime | None = None) -> date:
1606
- now = now or datetime.now(IST)
1607
- if is_trading_day(now.date()) and now.time() >= FIRST5_READY:
1608
- return now.date()
1609
- return previous_trading_day(now.date() - timedelta(days=1))
1610
-
1611
-
1612
- def expected_tplus1_date(now: datetime | None = None) -> date:
1613
- now = now or datetime.now(IST)
1614
- if is_trading_day(now.date()) and now.time() >= TPLUS1_READY:
1615
- return now.date()
1616
- return previous_trading_day(now.date() - timedelta(days=1))
1617
-
1618
-
1619
- def is_stale(latest: date | None, expected: date) -> bool:
1620
- return latest is None or latest < expected
1621
-
1622
-
1623
- def stale_data_status(now: datetime | None = None) -> dict[str, Any]:
1624
- now = now or datetime.now(IST)
1625
- expected_daily = expected_completed_daily_date(now)
1626
- expected_minutes = expected_minute_date(now)
1627
- expected_tplus1 = expected_tplus1_date(now)
1628
- latest_daily = latest_parquet_date(NIFTY_1D_PATH)
1629
- latest_minutes = latest_parquet_date(NIFTY_1M_PATH)
1630
- latest_t5 = latest_prediction_input_date(LATEST_PATH)
1631
- latest_tomorrow = latest_tomorrow_input_date()
1632
- latest_tplus1 = latest_prediction_input_date(TPLUS1_LATEST_PATH)
1633
- return {
1634
- "server_time_ist": now.isoformat(),
1635
- "expected_daily_date": expected_daily.isoformat(),
1636
- "expected_minute_date": expected_minutes.isoformat(),
1637
- "expected_tplus1_date": expected_tplus1.isoformat(),
1638
- "latest_daily_date": latest_daily.isoformat() if latest_daily else None,
1639
- "latest_minute_date": latest_minutes.isoformat() if latest_minutes else None,
1640
- "latest_t5_date": latest_t5.isoformat() if latest_t5 else None,
1641
- "latest_tomorrow_date": latest_tomorrow.isoformat() if latest_tomorrow else None,
1642
- "latest_tplus1_date": latest_tplus1.isoformat() if latest_tplus1 else None,
1643
- "daily_stale": is_stale(latest_daily, expected_daily),
1644
- "minutes_stale": is_stale(latest_minutes, expected_minutes),
1645
- "t5_stale": is_stale(latest_t5, expected_minutes),
1646
- "tomorrow_stale": is_stale(latest_tomorrow, expected_daily),
1647
- "tplus1_stale": is_stale(latest_tplus1, expected_tplus1),
1648
- }
1649
-
1650
-
1651
- def refresh_stale_data_once(now: datetime | None = None) -> dict[str, Any]:
1652
- now = now or datetime.now(IST)
1653
- status = stale_data_status(now)
1654
- if not any(status[key] for key in ("daily_stale", "minutes_stale", "t5_stale", "tomorrow_stale", "tplus1_stale")):
1655
- return {"status": "fresh", **status, "actions": []}
1656
- if not _stale_refresh_lock.acquire(blocking=False):
1657
- return {"status": "skipped", "reason": "stale refresh already running", **status, "actions": []}
1658
-
1659
- actions: list[dict[str, Any]] = []
1660
- try:
1661
- if status["minutes_stale"]:
1662
- minutes = fetch_yahoo_minutes(period="7d")
1663
- combined = append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
1664
- actions.append(
1665
- {
1666
- "name": "minutes",
1667
- "rows": int(len(combined)),
1668
- "latest_date": pd.to_datetime(combined["date"], errors="coerce").max().date().isoformat(),
1669
- }
1670
- )
1671
-
1672
- if status["daily_stale"]:
1673
- daily_info = refresh_daily_data()
1674
- outcomes = update_opening_outcomes_from_daily()
1675
- actions.append({"name": "daily", **daily_info})
1676
- actions.append({"name": "opening_outcomes", **outcomes})
1677
-
1678
- if status["daily_stale"] or status["tomorrow_stale"]:
1679
- try:
1680
- tomorrow = refresh_tomorrow_prediction(session_date=date.fromisoformat(status["expected_daily_date"]))
1681
- actions.append({"name": "tomorrow_prediction", "input_date": tomorrow.get("input_date")})
1682
- except Exception as exc:
1683
- actions.append({"name": "tomorrow_prediction", "error": str(exc)})
1684
-
1685
- if status["t5_stale"] and is_trading_day(now.date()) and now.time() >= FIRST5_READY:
1686
- prediction = refresh_first5_prediction(session_date=now.date())
1687
- actions.append({"name": "t5_prediction", "input_date": prediction.input_date})
1688
-
1689
- if status["tplus1_stale"] and is_trading_day(now.date()) and now.time() >= TPLUS1_READY:
1690
- prediction = refresh_tplus1_prediction(session_date=now.date())
1691
- actions.append({"name": "tplus1_prediction", "input_date": prediction.get("input_date")})
1692
-
1693
- clear_dashboard_payload_cache()
1694
- refreshed_status = stale_data_status(datetime.now(IST))
1695
- return {"status": "refreshed", **refreshed_status, "actions": actions}
1696
- finally:
1697
- _stale_refresh_lock.release()
1698
-
1699
-
1700
- def next_ist_run_at(run_time: time = time(9, 20), now: datetime | None = None) -> datetime:
 
 
 
1701
  now = now or datetime.now(IST)
1702
  target_day = now.date()
1703
  if now >= datetime.combine(target_day, run_time, tzinfo=IST):
 
1
  from __future__ import annotations
2
 
3
+ import json
4
+ import copy
5
+ import os
6
+ import sys
7
+ import threading
8
  from dataclasses import dataclass
9
  from datetime import date, datetime, time, timedelta
10
  from functools import lru_cache
 
12
  from typing import Any
13
  from zoneinfo import ZoneInfo
14
 
15
+ import joblib
16
+ import numpy as np
17
+ import pandas as pd
18
+ from nifty_backend.yahoo_history_client import YahooHistoryClient
19
 
20
  try:
21
  import pandas_market_calendars as mcal
 
24
 
25
 
26
  IST = ZoneInfo("Asia/Kolkata")
27
+ YAHOO_NIFTY_SYMBOL = "^NSEI"
28
+ MARKET_CLOSE = time(15, 30)
29
+ FIRST5_READY = time(9, 20)
30
+ CLOSE_REFRESH_READY = time(15, 45)
31
+ TPLUS1_READY = time(14, 30)
32
+ STALE_CHECK_INTERVAL_SECONDS = 5
33
+ BACKEND_ROOT = Path(__file__).resolve().parents[1]
34
+ DATA_DIR = BACKEND_ROOT / "data"
35
+ MODEL_DIR = BACKEND_ROOT / "models"
36
+ YAHOO_CACHE_PATH = MODEL_DIR / "yahoo_history_cache.sqlite3"
37
  OPENING_DATASET_PATH = DATA_DIR / "opening_direction_training_dataset.parquet"
38
  NIFTY_1M_PATH = DATA_DIR / "nifty50_1m.parquet"
39
  NIFTY_1D_PATH = DATA_DIR / "nifty50_1d.parquet"
 
43
  TOMORROW_MODEL_PATH = MODEL_DIR / "nifty_tomorrow_direction_model.joblib"
44
  TOMORROW_LATEST_PATH = MODEL_DIR / "tomorrow_latest_prediction.csv"
45
  TOMORROW_SUMMARY_PATH = MODEL_DIR / "tomorrow_summary.json"
46
+ TOMORROW_TEST_PREDICTIONS_PATH = DATA_DIR / "tomorrow_test_predictions.parquet"
47
+ TOMORROW_PREDICTION_HISTORY_PATH = MODEL_DIR / "tomorrow_prediction_history.parquet"
48
+ FORECASTING_PROJECT_ROOT = Path(
49
+ os.environ.get(
50
+ "FORECASTING_PROJECT_ROOT",
51
+ str(BACKEND_ROOT.parent.parent / "forecasting project"),
52
+ )
53
+ )
54
+ DAILY_FORECASTER_OUTPUT_DIR = MODEL_DIR / "nifty_forecaster" / "outputs"
55
+ DAILY_FORECASTER_SUMMARY_PATH = DAILY_FORECASTER_OUTPUT_DIR / "forecaster_summary.json"
56
+ DAILY_FORECASTER_LATEST_PATH = DAILY_FORECASTER_OUTPUT_DIR / "forecaster_latest.csv"
57
+ DAILY_FORECASTER_PREDICTIONS_PATH = DAILY_FORECASTER_OUTPUT_DIR / "forecaster_test_predictions.csv"
58
+ MFE_SOURCE_OUTPUT_DIR = FORECASTING_PROJECT_ROOT / "Code" / "models" / "nifty_opening_mfe_regressor" / "outputs"
59
+ MFE_OUTPUT_DIR = MODEL_DIR / "nifty_opening_mfe_regressor" / "outputs"
60
+ MFE_SUMMARY_PATH = MFE_OUTPUT_DIR / "summary.json"
61
+ MFE_LATEST_PATH = MFE_OUTPUT_DIR / "latest_prediction.csv"
62
+ MFE_TEST_PREDICTIONS_PATH = MFE_OUTPUT_DIR / "test_predictions.csv"
63
+ TPLUS1_MODEL_PATH = MODEL_DIR / "nifty_1420_tplus1_logistic_model.joblib"
64
  TPLUS1_LATEST_PATH = MODEL_DIR / "tplus1_latest_prediction.csv"
65
  TPLUS1_SUMMARY_PATH = MODEL_DIR / "tplus1_summary.json"
66
  TPLUS1_TEST_PREDICTIONS_PATH = DATA_DIR / "tplus1_test_predictions.parquet"
67
+ TPLUS1_PREDICTION_HISTORY_PATH = MODEL_DIR / "tplus1_prediction_history.parquet"
68
+ T5_PREDICTION_HISTORY_PATH = MODEL_DIR / "t5_prediction_history.parquet"
69
  REFRESH_STATE_PATH = MODEL_DIR / "refresh_state.json"
70
  REFRESH_WAITING = "waiting_second_payload"
71
  REFRESH_REFRESHING = "refreshing"
 
89
  },
90
  ]
91
 
92
+ _dashboard_payload_lock = threading.Lock()
93
+ _stale_refresh_lock = threading.Lock()
94
 
95
 
96
  def utc_now_iso() -> str:
 
187
  return pd.Timestamp(schedule.index[-1]).date()
188
 
189
 
190
+ def last_n_trading_sessions(end_day: date, count: int) -> list[date]:
191
+ """Return the last ``count`` NSE sessions ending on (or before) ``end_day``."""
192
+ sessions: list[date] = []
193
+ cursor = end_day
194
+ guard = 0
195
+ while len(sessions) < count and guard < count * 12:
196
+ guard += 1
197
+ if is_trading_day(cursor):
198
+ sessions.append(cursor)
199
+ if len(sessions) >= count:
200
+ break
201
+ cursor = previous_trading_day(cursor - timedelta(days=1))
202
+ sessions.reverse()
203
+ return sessions
204
+
205
+
206
+ def _track_record_end_session(now: datetime | None = None) -> date:
207
+ """Latest session the track record should score (today after the close refresh window)."""
208
+ now = now or datetime.now(IST)
209
+ today = now.date()
210
+ if is_trading_day(today) and now.time() >= CLOSE_REFRESH_READY:
211
+ return today
212
+ return expected_completed_daily_date(now)
213
+
214
+
215
  class ProbabilityBlend:
216
  def __init__(self, models: list[Any], weights: np.ndarray):
217
  self.models = models
 
311
  return df.sort_values("date").reset_index(drop=True)
312
 
313
 
314
+ def normalize_yahoo_frame(df: pd.DataFrame) -> pd.DataFrame:
315
  if df.empty:
316
  return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume"])
317
  if isinstance(df.columns, pd.MultiIndex):
 
337
  for src, dst in rename.items():
338
  if src in df.columns and dst not in out.columns:
339
  out[dst] = pd.to_numeric(df[src], errors="coerce")
340
+ return out.dropna(subset=["date", "open", "high", "low", "close"]).sort_values("date")
341
+
342
+
343
+ @lru_cache(maxsize=1)
344
+ def yahoo_history_client() -> YahooHistoryClient:
345
+ return YahooHistoryClient(cache_path=YAHOO_CACHE_PATH)
346
+
347
+
348
+ def period_start(period: str, *, end: datetime) -> datetime:
349
+ text = str(period).strip().lower()
350
+ units = {
351
+ "d": "days",
352
+ "wk": "weeks",
353
+ "mo": "months",
354
+ "y": "years",
355
+ }
356
+ for suffix, unit in units.items():
357
+ if text.endswith(suffix):
358
+ raw_value = text[: -len(suffix)]
359
+ if not raw_value.isdigit():
360
+ break
361
+ value = int(raw_value)
362
+ if unit == "days":
363
+ return end - timedelta(days=value)
364
+ if unit == "weeks":
365
+ return end - timedelta(weeks=value)
366
+ if unit == "months":
367
+ return end - timedelta(days=value * 31)
368
+ if unit == "years":
369
+ return end - timedelta(days=value * 366)
370
+ raise ValueError(f"Unsupported Yahoo period: {period!r}")
371
+
372
+
373
+ def yahoo_history_to_ohlcv(frame: pd.DataFrame, *, daily: bool) -> pd.DataFrame:
374
+ if frame.empty:
375
+ return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume"])
376
+ out = frame.rename(columns={"timestamp": "date"}).copy()
377
+ out["date"] = pd.to_datetime(out["date"], errors="coerce")
378
+ if daily:
379
+ out["date"] = out["date"].dt.normalize()
380
+ for column in ("open", "high", "low", "close", "volume"):
381
+ out[column] = pd.to_numeric(out[column], errors="coerce")
382
+ return (
383
+ out[["date", "open", "high", "low", "close", "volume"]]
384
+ .dropna(subset=["date", "open", "high", "low", "close"])
385
+ .drop_duplicates("date", keep="last")
386
+ .sort_values("date")
387
+ .reset_index(drop=True)
388
+ )
389
+
390
+
391
+ def fetch_yahoo_minutes(period: str = "5d") -> pd.DataFrame:
392
+ end = datetime.now(IST).replace(tzinfo=None) + timedelta(minutes=5)
393
+ start = period_start(period, end=end)
394
+ raw = yahoo_history_client().fetch_history(
395
+ YAHOO_NIFTY_SYMBOL,
396
+ interval="1m",
397
+ start=start,
398
+ end=end,
399
+ include_prepost=False,
400
+ )
401
+ return yahoo_history_to_ohlcv(raw, daily=False)
402
+
403
+
404
+ def fetch_yahoo_daily(period: str = "1mo") -> pd.DataFrame:
405
+ end = datetime.now(IST).replace(tzinfo=None) + timedelta(days=1)
406
+ start = period_start(period, end=end)
407
+ raw = yahoo_history_client().fetch_history(
408
+ YAHOO_NIFTY_SYMBOL,
409
+ interval="1d",
410
+ start=start,
411
+ end=end,
412
+ include_prepost=False,
413
+ )
414
+ return yahoo_history_to_ohlcv(raw, daily=True)
415
 
416
 
417
  def append_parquet_rows(path: Path, new_rows: pd.DataFrame, subset: list[str]) -> pd.DataFrame:
 
429
  return combined
430
 
431
 
432
+ def append_prediction_history(path: Path, row: dict[str, Any], subset: list[str]) -> pd.DataFrame:
433
+ frame = pd.DataFrame([row])
434
+ return append_parquet_rows(path, frame, subset)
435
+
436
+
437
  def latest_parquet_date(path: Path) -> date | None:
438
  if not path.exists():
439
  return None
 
565
  is_overridden=is_overridden,
566
  )
567
  pd.DataFrame([prediction.to_dict()]).to_csv(LATEST_PATH, index=False)
568
+ _record_prediction_history(
569
+ T5_PREDICTION_HISTORY_PATH,
570
+ {
571
+ **prediction.to_dict(),
572
+ "target_date": prediction.input_date,
573
+ "source": "live",
574
+ },
575
+ ["target_date"],
576
+ )
577
  return prediction
578
 
579
 
 
600
  return dict(_latest_saved_prediction_cached(_file_cache_key(LATEST_PATH), _file_cache_key(MODEL_DIR / "summary.json")))
601
 
602
 
603
+ def _latest_saved_prediction_uncached() -> dict[str, Any]:
604
  if LATEST_PATH.exists():
605
  return pd.read_csv(LATEST_PATH).iloc[-1].to_dict()
606
  summary_path = MODEL_DIR / "summary.json"
607
  if summary_path.exists():
608
  return json.loads(summary_path.read_text(encoding="utf-8"))
609
+ raise FileNotFoundError("No latest prediction is available yet.")
610
+
611
+
612
+ def _read_daily_forecaster_summary() -> dict[str, Any] | None:
613
+ if not DAILY_FORECASTER_SUMMARY_PATH.exists():
614
+ return None
615
+ raw = json.loads(DAILY_FORECASTER_SUMMARY_PATH.read_text(encoding="utf-8"))
616
+ if isinstance(raw, list):
617
+ matches = [row for row in raw if row.get("symbol") == "NIFTY 50"]
618
+ summary = dict(matches[0] if matches else raw[0])
619
+ elif isinstance(raw, dict):
620
+ summary = dict(raw)
621
+ else:
622
+ return None
623
+ config = summary.get("config") if isinstance(summary.get("config"), dict) else {}
624
+ summary.setdefault("symbol", "NIFTY 50")
625
+ summary.setdefault("horizon", "daily")
626
+ summary.setdefault("horizon_bars", 1)
627
+ summary["model_name"] = "nifty_tomorrow_direction_model"
628
+ summary["source_model"] = str(config.get("name") or summary.get("source_model") or "locked_multiwindow_nifty50_ensemble")
629
+ summary["target"] = "next trading session NIFTY 50 direction"
630
+ summary["artifact_type"] = "daily_forecaster_outputs"
631
+ summary["artifact_source"] = str(DAILY_FORECASTER_OUTPUT_DIR)
632
+ return summary
633
+
634
+
635
+ def _read_daily_forecaster_latest(summary: dict[str, Any]) -> dict[str, Any] | None:
636
+ if not DAILY_FORECASTER_LATEST_PATH.exists():
637
+ return None
638
+ latest = pd.read_csv(DAILY_FORECASTER_LATEST_PATH)
639
+ if latest.empty:
640
+ return None
641
+ if "symbol" in latest.columns:
642
+ filtered = latest[latest["symbol"].astype(str) == "NIFTY 50"]
643
+ if not filtered.empty:
644
+ latest = filtered
645
+ row = {k: (None if pd.isna(v) else v) for k, v in latest.iloc[-1].to_dict().items()}
646
+ input_date = row.get("latest_forecast_date") or row.get("input_date")
647
+ target_date = row.get("target_date")
648
+ if not target_date and input_date:
649
+ try:
650
+ target_date = next_trading_day(date.fromisoformat(str(input_date)[:10]) + timedelta(days=1)).isoformat()
651
+ except Exception:
652
+ target_date = None
653
+ prob_up = row.get("latest_forecast_prob_up", row.get("prob_up"))
654
+ prediction = row.get("latest_forecast_signal", row.get("prediction"))
655
+ threshold = row.get("threshold", summary.get("threshold"))
656
+ confidence = row.get("confidence")
657
+ if confidence is None and prob_up is not None:
658
+ try:
659
+ confidence = float(max(float(prob_up), 1.0 - float(prob_up)))
660
+ except Exception:
661
+ confidence = None
662
+ return {
663
+ "input_date": input_date,
664
+ "target_date": target_date,
665
+ "prediction": prediction,
666
+ "prob_up": prob_up,
667
+ "confidence": confidence,
668
+ "threshold": threshold,
669
+ "model_name": "nifty_tomorrow_direction_model",
670
+ "source_model": summary.get("source_model", "locked_multiwindow_nifty50_ensemble"),
671
+ "validation_accuracy": summary.get("validation_accuracy"),
672
+ "test_accuracy": summary.get("test_accuracy"),
673
+ "artifact_source": str(DAILY_FORECASTER_OUTPUT_DIR),
674
+ }
675
+
676
+
677
+ def _parse_iso_date(value: Any) -> date | None:
678
+ try:
679
+ return date.fromisoformat(str(value or "")[:10])
680
+ except Exception:
681
+ return None
682
+
683
+
684
+ def _archive_tomorrow_latest_to_history() -> None:
685
+ if not TOMORROW_LATEST_PATH.exists():
686
+ return
687
+ try:
688
+ row = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
689
+ cleaned = {k: (None if pd.isna(v) else v) for k, v in row.items()}
690
+ pred = str(cleaned.get("prediction", "")).upper()
691
+ if pred in {"UP", "DOWN"} and cleaned.get("target_date"):
692
+ _record_prediction_history(TOMORROW_PREDICTION_HISTORY_PATH, cleaned, ["target_date"])
693
+ except Exception:
694
+ pass
695
+
696
+
697
+ def _tomorrow_actual_outcome(
698
+ target_day: date,
699
+ day_close: float,
700
+ closes_by_date: dict[date, float],
701
+ ) -> tuple[float | None, str | None]:
702
+ """Return (move, direction) for Tomorrow scoring: close vs previous session close."""
703
+ prev_day = previous_trading_day(target_day - timedelta(days=1))
704
+ prev_close = closes_by_date.get(prev_day)
705
+ if prev_close is None or not np.isfinite(prev_close) or prev_close == 0:
706
+ return None, None
707
+ actual_move = (day_close - prev_close) / prev_close
708
+ actual_direction = "UP" if day_close > prev_close else "DOWN"
709
+ return actual_move, actual_direction
710
+
711
+
712
+ def _find_tomorrow_prediction_for_target(target_day: date) -> dict[str, Any] | None:
713
+ """Return the Tomorrow prediction that targets ``target_day``."""
714
+ target_iso = target_day.isoformat()
715
+ tom_history = _load_prediction_history(TOMORROW_PREDICTION_HISTORY_PATH)
716
+ if not tom_history.empty:
717
+ for col in ("date", "input_date", "target_date", "forecast_date"):
718
+ if col in tom_history.columns:
719
+ tom_history[col] = pd.to_datetime(tom_history[col], errors="coerce")
720
+
721
+ if not tom_history.empty and "target_date" in tom_history.columns:
722
+ rows = tom_history[tom_history["target_date"].dt.date == target_day]
723
+ if not rows.empty:
724
+ return rows.iloc[-1].to_dict()
725
+
726
+ if TOMORROW_LATEST_PATH.exists():
727
+ try:
728
+ row = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
729
+ if _parse_iso_date(row.get("target_date")) == target_day:
730
+ return row
731
+ except Exception:
732
+ pass
733
+
734
+ input_day = previous_trading_day(target_day - timedelta(days=1))
735
+ if not tom_history.empty and "input_date" in tom_history.columns:
736
+ rows = tom_history[tom_history["input_date"].dt.date == input_day]
737
+ if not rows.empty:
738
+ row = rows.iloc[-1].to_dict()
739
+ if _parse_iso_date(row.get("target_date")) in {None, target_day}:
740
+ return row
741
+
742
+ if TOMORROW_LATEST_PATH.exists():
743
+ try:
744
+ row = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
745
+ if _parse_iso_date(row.get("input_date")) == input_day:
746
+ return row
747
+ except Exception:
748
+ pass
749
+
750
+ tomorrow_test = load_tomorrow_test_predictions()
751
+ tomorrow_history = _load_prediction_history(TOMORROW_PREDICTION_HISTORY_PATH)
752
+ if not tomorrow_test.empty:
753
+ for col in ("target_date", "date"):
754
+ if col in tomorrow_test.columns:
755
+ test_dates = pd.to_datetime(tomorrow_test[col], errors="coerce").dt.date
756
+ rows = tomorrow_test[test_dates == target_day]
757
+ if not rows.empty:
758
+ return rows.iloc[-1].to_dict()
759
+
760
+ ledger = load_live_accuracy()
761
+ for entry in ledger.get("tomorrow", {}).get("entries", []):
762
+ if str(entry.get("date", ""))[:10] == target_iso:
763
+ pred = str(entry.get("prediction", "")).upper()
764
+ if pred in {"UP", "DOWN"}:
765
+ return {
766
+ "target_date": target_iso,
767
+ "prediction": pred,
768
+ "source": entry.get("source", "live"),
769
+ }
770
+ return None
771
+
772
+
773
+ def sync_daily_forecaster_outputs() -> dict[str, Any] | None:
774
+ summary = _read_daily_forecaster_summary()
775
+ if summary is None:
776
+ return None
777
+ latest = _read_daily_forecaster_latest(summary)
778
+ TOMORROW_SUMMARY_PATH.write_text(json.dumps(summary, indent=2), encoding="utf-8")
779
+ if latest is not None:
780
+ _archive_tomorrow_latest_to_history()
781
+ keep_existing = False
782
+ if TOMORROW_LATEST_PATH.exists():
783
+ try:
784
+ existing = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
785
+ existing_input = _parse_iso_date(existing.get("input_date"))
786
+ forecaster_input = _parse_iso_date(latest.get("input_date"))
787
+ existing_pred = str(existing.get("prediction", "")).upper()
788
+ if (
789
+ existing_input is not None
790
+ and forecaster_input is not None
791
+ and existing_input > forecaster_input
792
+ and existing_pred in {"UP", "DOWN"}
793
+ ):
794
+ keep_existing = True
795
+ except Exception:
796
+ keep_existing = False
797
+ if not keep_existing:
798
+ pd.DataFrame([latest]).to_csv(TOMORROW_LATEST_PATH, index=False)
799
+ if DAILY_FORECASTER_PREDICTIONS_PATH.exists():
800
+ predictions = pd.read_csv(DAILY_FORECASTER_PREDICTIONS_PATH)
801
+ if "symbol" in predictions.columns:
802
+ predictions = predictions[predictions["symbol"].astype(str) == "NIFTY 50"].copy()
803
+ if not predictions.empty:
804
+ if "pred" in predictions.columns and "prediction" not in predictions.columns:
805
+ predictions["prediction"] = np.where(pd.to_numeric(predictions["pred"], errors="coerce") == 1, "UP", "DOWN")
806
+ if "correct" not in predictions.columns and {"target", "pred"}.issubset(predictions.columns):
807
+ predictions["correct"] = (
808
+ pd.to_numeric(predictions["target"], errors="coerce")
809
+ == pd.to_numeric(predictions["pred"], errors="coerce")
810
+ )
811
+ predictions.to_parquet(TOMORROW_TEST_PREDICTIONS_PATH, index=False)
812
+ artifact = {
813
+ "artifact_type": "daily_forecaster_outputs",
814
+ "model_name": "nifty_tomorrow_direction_model",
815
+ "source_model": summary.get("source_model", "locked_multiwindow_nifty50_ensemble"),
816
+ "threshold": float(summary.get("threshold", 0.54)),
817
+ "validation_accuracy": summary.get("validation_accuracy"),
818
+ "test_accuracy": summary.get("test_accuracy"),
819
+ "validation_prob_std": summary.get("validation_prob_std"),
820
+ "test_prob_std": summary.get("test_prob_std"),
821
+ "test_prob_min": summary.get("test_prob_min"),
822
+ "test_prob_max": summary.get("test_prob_max"),
823
+ "artifact_source": str(DAILY_FORECASTER_OUTPUT_DIR),
824
+ }
825
+ joblib.dump(artifact, TOMORROW_MODEL_PATH)
826
+ return latest or summary
827
+
828
+
829
+ def load_tomorrow_model_artifact() -> dict[str, Any]:
830
+ synced = sync_daily_forecaster_outputs()
831
+ if synced is not None and TOMORROW_MODEL_PATH.exists():
832
+ return joblib.load(TOMORROW_MODEL_PATH)
833
+ if TOMORROW_MODEL_PATH.exists():
834
+ return joblib.load(TOMORROW_MODEL_PATH)
835
+ summary = load_tomorrow_summary()
836
  return {
837
  "artifact_type": "daily_forecaster_snapshot",
838
  "model_name": summary.get("model_name", "nifty_tomorrow_direction_model"),
 
841
  }
842
 
843
 
844
+ def load_tomorrow_summary() -> dict[str, Any]:
845
+ synced = sync_daily_forecaster_outputs()
846
+ if synced is not None and TOMORROW_SUMMARY_PATH.exists():
847
+ return json.loads(TOMORROW_SUMMARY_PATH.read_text(encoding="utf-8"))
848
+ if TOMORROW_SUMMARY_PATH.exists():
849
+ return json.loads(TOMORROW_SUMMARY_PATH.read_text(encoding="utf-8"))
850
+ return {
851
+ "model_name": "nifty_tomorrow_direction_model",
852
+ "source_model": "locked_multiwindow_nifty50_ensemble",
853
+ "target": "next trading session NIFTY 50 direction",
854
+ "threshold": 0.54,
855
+ "validation_accuracy": 0.5673758865248227,
856
+ "test_accuracy": 0.6451612903225806,
857
+ "baseline_accuracy": 0.5053763440860215,
858
+ "n_test": 186,
859
+ "feature_count": 204,
860
+ }
861
+
862
+
863
+ def latest_tomorrow_prediction() -> dict[str, Any]:
864
+ sync_daily_forecaster_outputs()
865
+ latest_daily = latest_parquet_date(NIFTY_1D_PATH)
866
+ expected_daily = expected_completed_daily_date()
867
+ valid_daily = min(latest_daily, expected_daily) if latest_daily and expected_daily else (expected_daily or latest_daily)
868
+
869
+ if TOMORROW_LATEST_PATH.exists():
870
+ row = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
871
+ cleaned = {k: (None if pd.isna(v) else v) for k, v in row.items()}
872
+ try:
873
+ input_day = date.fromisoformat(str(cleaned.get("input_date"))[:10])
874
+ except Exception:
875
+ input_day = None
876
+ if valid_daily is not None and (input_day is None or input_day < valid_daily):
877
+ try:
878
+ refreshed = refresh_tomorrow_prediction(session_date=valid_daily)
879
+ try:
880
+ refreshed_day = date.fromisoformat(str(refreshed.get("input_date"))[:10])
881
+ except Exception:
882
+ refreshed_day = None
883
+ if refreshed_day is not None and refreshed_day >= valid_daily:
884
+ return refreshed
885
+ except Exception:
886
+ pass
887
+ return cleaned
888
+ summary = load_tomorrow_summary()
889
+ try:
890
+ summary_input_day = date.fromisoformat(str(summary.get("latest_forecast_date"))[:10])
891
+ except Exception:
892
+ summary_input_day = None
893
+ if valid_daily is not None and (summary_input_day is None or summary_input_day < valid_daily):
894
+ try:
895
+ return refresh_tomorrow_prediction(session_date=valid_daily)
896
+ except Exception:
897
+ pass
898
+ return {
899
+ "input_date": summary.get("latest_forecast_date"),
900
+ "target_date": None,
901
  "prediction": summary.get("latest_forecast_signal"),
902
  "prob_up": summary.get("latest_forecast_prob_up"),
903
  "confidence": None,
 
1053
  return adjusted
1054
 
1055
 
1056
+ def refresh_tplus1_prediction(session_date: date | None = None) -> dict[str, Any]:
1057
+ if not TPLUS1_MODEL_PATH.exists():
1058
+ raise FileNotFoundError(f"Missing T+1 model artifact: {TPLUS1_MODEL_PATH}")
1059
+ payload = joblib.load(TPLUS1_MODEL_PATH)
1060
+ features = payload["features"]
1061
+ threshold = float(payload["threshold"])
1062
+ frame = _add_tplus1_target_features(_build_tplus1_session_features(_minute_frame_for_tplus1()))
1063
+ if session_date is not None:
1064
+ row = frame[pd.to_datetime(frame["date"], errors="coerce").dt.date == session_date].tail(1)
1065
+ else:
1066
+ row = frame.tail(1)
1067
+ if row.empty:
1068
+ minutes = fetch_yahoo_minutes(period="7d")
1069
+ append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
1070
+ frame = _add_tplus1_target_features(_build_tplus1_session_features(_minute_frame_for_tplus1()))
1071
+ if session_date is not None:
1072
+ row = frame[pd.to_datetime(frame["date"], errors="coerce").dt.date == session_date].tail(1)
1073
+ else:
1074
+ row = frame.tail(1)
1075
+ if row.empty:
1076
+ raise RuntimeError("No complete 14:00-14:20 window is available for T+1 prediction.")
1077
  missing = [col for col in features if col not in row.columns]
1078
  if missing:
1079
  raise RuntimeError(f"T+1 feature row is missing model features: {missing[:5]}")
 
1099
  "validation_accuracy": summary.get("validation_accuracy"),
1100
  "test_accuracy": summary.get("test_accuracy"),
1101
  "accuracy_goal": summary.get("accuracy_goal"),
1102
+ "source": "live",
1103
  }
1104
  pd.DataFrame([out]).to_csv(TPLUS1_LATEST_PATH, index=False)
1105
+ _record_prediction_history(TPLUS1_PREDICTION_HISTORY_PATH, out, ["target_date"])
1106
  clear_dashboard_payload_cache()
1107
  return out
1108
 
 
1129
  return float(np.clip(score, 0.35, 0.65))
1130
 
1131
 
1132
+ def refresh_tomorrow_prediction(session_date: date | None = None) -> dict[str, Any]:
1133
+ _archive_tomorrow_latest_to_history()
1134
+ synced = sync_daily_forecaster_outputs()
1135
+ if synced is not None and TOMORROW_LATEST_PATH.exists():
1136
+ latest = pd.read_csv(TOMORROW_LATEST_PATH).iloc[-1].to_dict()
1137
+ cleaned = {k: (None if pd.isna(v) else v) for k, v in latest.items()}
1138
+ cleaned["source"] = "live"
1139
+ _record_prediction_history(TOMORROW_PREDICTION_HISTORY_PATH, cleaned, ["target_date"])
1140
+ if session_date is None:
1141
+ clear_dashboard_payload_cache()
1142
+ return cleaned
1143
+ try:
1144
+ input_day = date.fromisoformat(str(cleaned.get("input_date"))[:10])
1145
+ except Exception:
1146
+ input_day = None
1147
+ if input_day is not None and (session_date is None or input_day >= session_date):
1148
+ clear_dashboard_payload_cache()
1149
+ return cleaned
1150
+ summary = load_tomorrow_summary()
1151
+ artifact = load_tomorrow_model_artifact()
1152
  daily = pd.read_parquet(NIFTY_1D_PATH)
1153
  daily["date"] = pd.to_datetime(daily["date"], errors="coerce").dt.normalize()
1154
  daily = daily.dropna(subset=["date"]).sort_values("date")
 
1172
  "source_model": str(summary.get("source_model", "tuned_daily_forest_single")),
1173
  "validation_accuracy": float(summary.get("validation_accuracy", 0.5780141843971631)),
1174
  "test_accuracy": float(summary.get("test_accuracy", 0.6182795698924731)),
1175
+ "source": "live",
1176
  }
1177
  pd.DataFrame([row]).to_csv(TOMORROW_LATEST_PATH, index=False)
1178
+ _record_prediction_history(TOMORROW_PREDICTION_HISTORY_PATH, row, ["target_date"])
1179
  summary = dict(summary)
1180
+ summary.update(
1181
+ {
1182
+ "latest_forecast_date": row["input_date"],
1183
  "latest_forecast_for": f"next trading session {row['target_date']}",
1184
  "latest_forecast_prob_up": row["prob_up"],
1185
  "latest_forecast_signal": row["prediction"],
1186
  "latest_target_date": row["target_date"],
1187
+ }
1188
+ )
1189
+ TOMORROW_SUMMARY_PATH.write_text(json.dumps(summary, indent=2), encoding="utf-8")
1190
+ clear_dashboard_payload_cache()
1191
+ return row
1192
 
1193
 
1194
  def _json_ready_frame(df: pd.DataFrame, limit: int | None = None) -> list[dict[str, Any]]:
 
1202
  return out.to_dict(orient="records")
1203
 
1204
 
1205
+
1206
+
1207
  def load_model_summary() -> dict[str, Any]:
1208
  summary_path = MODEL_DIR / "summary.json"
1209
  if not summary_path.exists():
 
1266
  _file_cache_key(MODEL_DIR / "candidate_results.csv"),
1267
  _file_cache_key(NIFTY_1M_PATH),
1268
  _file_cache_key(LIVE_ACCURACY_PATH),
1269
+ _file_cache_key(TOMORROW_PREDICTION_HISTORY_PATH),
1270
  )
1271
  with _dashboard_payload_lock:
1272
  return copy.deepcopy(_dashboard_payload_cached(key))
1273
 
1274
 
1275
+ def warm_dashboard_payload_cache() -> None:
1276
+ dashboard_payload()
1277
+
1278
+
1279
+ def _load_forecaster_predictions_by_target() -> dict[date, dict[str, Any]]:
1280
+ path = DAILY_FORECASTER_PREDICTIONS_PATH
1281
+ if not path.exists():
1282
+ return {}
1283
+ try:
1284
+ frame = pd.read_csv(path)
1285
+ except Exception:
1286
+ return {}
1287
+ if frame.empty:
1288
+ return {}
1289
+ if "symbol" in frame.columns:
1290
+ frame = frame[frame["symbol"].astype(str) == "NIFTY 50"].copy()
1291
+ indexed: dict[date, dict[str, Any]] = {}
1292
+ for _, row in frame.iterrows():
1293
+ target_day = _parse_iso_date(row.get("target_date"))
1294
+ if target_day is None:
1295
+ continue
1296
+ pred_value = row.get("pred")
1297
+ if pd.isna(pred_value) and "raw_pred" in row:
1298
+ pred_value = row.get("raw_pred")
1299
+ try:
1300
+ pred_int = int(pred_value)
1301
+ except Exception:
1302
+ continue
1303
+ prob_up = row.get("prob_up")
1304
+ try:
1305
+ prob_up = float(prob_up) if pd.notna(prob_up) else None
1306
+ except Exception:
1307
+ prob_up = None
1308
+ indexed[target_day] = {
1309
+ "prediction": "UP" if pred_int == 1 else "DOWN",
1310
+ "prob_up": prob_up,
1311
+ "forecast_date": row.get("forecast_date"),
1312
+ "source": "Tomorrow (forecaster)",
1313
+ }
1314
+ return indexed
1315
+
1316
+
1317
+ def _load_track_record_daily_rows() -> pd.DataFrame:
1318
+ frames: list[pd.DataFrame] = []
1319
+ if NIFTY_1D_PATH.exists():
1320
+ try:
1321
+ frames.append(pd.read_parquet(NIFTY_1D_PATH))
1322
+ except Exception:
1323
+ pass
1324
+ try:
1325
+ yahoo_daily = fetch_yahoo_daily(period="3mo")
1326
+ if not yahoo_daily.empty:
1327
+ frames.append(yahoo_daily)
1328
+ try:
1329
+ append_parquet_rows(NIFTY_1D_PATH, yahoo_daily, ["date"])
1330
+ except Exception:
1331
+ pass
1332
+ except Exception:
1333
+ pass
1334
+
1335
+ if not frames:
1336
+ return pd.DataFrame()
1337
+
1338
+ combined = pd.concat(frames, ignore_index=True)
1339
+ combined["date"] = pd.to_datetime(combined["date"], errors="coerce").dt.normalize()
1340
+ combined = combined.dropna(subset=["date"]).sort_values("date")
1341
+ combined = combined.drop_duplicates(subset=["date"], keep="last")
1342
+ combined = combined[
1343
+ combined["close"].map(lambda value: np.isfinite(float(value)) if pd.notna(value) else False)
1344
+ ].copy()
1345
+ return combined.reset_index(drop=True)
1346
+
1347
+
1348
+ def _rolling_tomorrow_prediction(
1349
+ input_day: date,
1350
+ daily_rows: pd.DataFrame,
1351
+ threshold: float,
1352
+ fallback_prob: float,
1353
+ ) -> tuple[str, float]:
1354
+ history = daily_rows[daily_rows["date"].dt.date <= input_day].copy()
1355
+ prob_up = _tomorrow_probability_from_daily(history, fallback_prob)
1356
+ prediction = "UP" if prob_up >= threshold else "DOWN"
1357
+ return prediction, float(prob_up)
1358
+
1359
+
1360
+ def build_prediction_track_record(
1361
+ sessions: int = 10,
1362
+ ) -> list[dict[str, Any]]:
1363
+ summary = load_tomorrow_summary()
1364
+ artifact = load_tomorrow_model_artifact()
1365
+ threshold = float(artifact.get("threshold", summary.get("threshold", 0.534)))
1366
+ fallback_prob = float(summary.get("latest_forecast_prob_up", 0.49900560447008563))
1367
+ forecaster_by_target = _load_forecaster_predictions_by_target()
1368
+
1369
+ daily_rows = _load_track_record_daily_rows()
1370
+ if daily_rows.empty:
1371
+ return []
1372
+
1373
+ closes_by_date = {
1374
+ row["date"].date(): float(row["close"])
1375
+ for _, row in daily_rows.iterrows()
1376
+ if pd.notna(row["close"]) and np.isfinite(float(row["close"]))
1377
+ }
1378
+
1379
+ end_session = _track_record_end_session()
1380
+ available_through = max(
1381
+ (day for day in closes_by_date if day <= end_session),
1382
+ default=None,
1383
+ )
1384
+ if available_through is None:
1385
+ return []
1386
+ if available_through < end_session:
1387
+ end_session = available_through
1388
+
1389
+ session_dates = last_n_trading_sessions(end_session, sessions)
1390
+
1391
+ records: list[dict[str, Any]] = []
1392
+ for target_day in session_dates:
1393
+ day_close = closes_by_date.get(target_day)
1394
+ if day_close is None:
1395
+ continue
1396
+ actual_move, actual_direction = _tomorrow_actual_outcome(target_day, day_close, closes_by_date)
1397
+ if actual_direction is None:
1398
+ continue
1399
+
1400
+ input_day = previous_trading_day(target_day - timedelta(days=1))
1401
+ cached = forecaster_by_target.get(target_day)
1402
+ if cached and cached.get("prediction") in {"UP", "DOWN"}:
1403
+ prediction = cached["prediction"]
1404
+ prob_up = cached.get("prob_up")
1405
+ source = cached.get("source", "Tomorrow (forecaster)")
1406
+ else:
1407
+ prediction, prob_up = _rolling_tomorrow_prediction(
1408
+ input_day,
1409
+ daily_rows,
1410
+ threshold,
1411
+ fallback_prob,
1412
+ )
1413
+ source = "Tomorrow (rolling)"
1414
+
1415
+ if prediction not in {"UP", "DOWN"}:
1416
+ continue
1417
+
1418
+ records.append(
1419
+ {
1420
+ "date": target_day.isoformat(),
1421
+ "input_date": input_day.isoformat(),
1422
+ "prediction": prediction,
1423
+ "prediction_source": source,
1424
+ "prob_up": prob_up,
1425
+ "actual_move": actual_move,
1426
+ "actual_direction": actual_direction,
1427
+ "correct": prediction == actual_direction,
1428
+ }
1429
+ )
1430
+ return records[-sessions:]
1431
+
1432
+
1433
+ @lru_cache(maxsize=4)
1434
+ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...]) -> dict[str, Any]:
1435
  summary = load_model_summary()
1436
  t5_latest = _latest_saved_prediction_uncached()
1437
  tomorrow_summary = load_tomorrow_summary()
1438
  tomorrow_latest = latest_tomorrow_prediction()
1439
  tplus1_summary = load_tplus1_summary()
1440
  tplus1_latest = latest_tplus1_prediction()
 
1441
  t5_test = load_test_predictions()
1442
  tomorrow_test = load_tomorrow_test_predictions()
1443
+ tomorrow_history = _load_prediction_history(TOMORROW_PREDICTION_HISTORY_PATH)
1444
  tplus1_test = load_tplus1_test_predictions()
1445
  daily = pd.read_parquet(NIFTY_1D_PATH)
1446
  daily["date"] = pd.to_datetime(daily["date"], errors="coerce")
1447
  daily = daily.sort_values("date").tail(180)
 
 
1448
 
1449
  if not t5_test.empty:
1450
+ recent_accuracy = float(t5_test.tail(40)["correct"].mean())
 
 
 
 
 
 
 
 
1451
  else:
 
1452
  recent_accuracy = None
 
 
1453
 
1454
  if not tomorrow_test.empty:
1455
  tomorrow_recent = tomorrow_test.tail(40).copy()
 
1459
  tomorrow_recent["correct"] = pd.to_numeric(tomorrow_recent["target"], errors="coerce") == pd.to_numeric(tomorrow_recent["pred"], errors="coerce")
1460
  tomorrow_accuracy = float(tomorrow_recent["correct"].mean()) if "correct" in tomorrow_recent.columns else tomorrow_summary.get("test_accuracy")
1461
  else:
 
1462
  tomorrow_accuracy = tomorrow_summary.get("test_accuracy")
1463
 
1464
  model_metrics = [
 
1493
  "test_rows": int(len(t5_test)) if not t5_test.empty else int(summary.get("test_rows") or 0),
1494
  },
1495
  ]
1496
+ metrics = {
1497
+ "validation_accuracy": tomorrow_summary.get("validation_accuracy"),
1498
+ "test_accuracy": tomorrow_summary.get("test_accuracy"),
1499
  "baseline_test_accuracy": tomorrow_summary.get("baseline_accuracy"),
1500
  "validation_auc": summary.get("validation_auc"),
1501
  "test_auc": summary.get("test_auc"),
1502
  "test_brier": summary.get("test_brier"),
1503
  "feature_count": tomorrow_summary.get("feature_count"),
1504
  "recent_accuracy": tomorrow_accuracy,
1505
+ "recent_accuracy_days": int(len(tomorrow_test.tail(40))) if not tomorrow_test.empty else 0,
1506
+ "total_test_days": int(tomorrow_summary.get("n_test") or len(tomorrow_test) or 0),
1507
+ "models": model_metrics,
1508
+ }
1509
+ return {
1510
+ "timestamp": datetime.now(ZoneInfo("Asia/Kolkata")).isoformat(),
1511
+ "predictions": {
1512
+ "t5": {
1513
+ "latest": t5_latest,
1514
+ "summary": summary,
1515
+ },
1516
+ "tomorrow": {
1517
+ "latest": tomorrow_latest,
1518
+ "summary": tomorrow_summary,
1519
+ },
1520
+ "tplus1": {
1521
+ "latest": tplus1_latest,
1522
+ "summary": tplus1_summary,
1523
+ },
1524
+ },
1525
  "metrics": metrics,
1526
+ "models": model_metrics,
1527
+ "live_accuracy": load_live_accuracy(),
 
 
1528
  "charts": {
1529
+ "daily_closes": _json_ready_frame(daily[["date", "close"]]),
1530
+ "t5_backtest": _json_ready_frame(t5_test, limit=400),
1531
+ "t5_recent_predictions": _json_ready_frame(t5_test.tail(40)),
1532
+ "tomorrow_backtest": _json_ready_frame(tomorrow_test, limit=400),
1533
+ "tomorrow_live_track_record": build_prediction_track_record(),
1534
+ "tomorrow_history_predictions": _json_ready_frame(tomorrow_history.tail(80)),
1535
+ "tplus1_backtest": _json_ready_frame(tplus1_test, limit=400),
 
 
 
 
 
 
 
 
 
 
 
 
 
1536
  },
1537
  }
1538
 
 
1551
  merged = merged.drop_duplicates(subset=["date"], keep="last").sort_values("date").reset_index(drop=True)
1552
  merged.to_parquet(OPENING_DATASET_PATH, index=False, compression="zstd")
1553
  prediction = predict_row(row)
 
1554
  return prediction
1555
 
1556
 
1557
  def refresh_daily_data() -> dict[str, Any]:
1558
  daily = fetch_yahoo_daily(period="1mo")
1559
  combined = append_parquet_rows(NIFTY_1D_PATH, daily, ["date"])
 
1560
  return {
1561
  "rows": int(len(combined)),
1562
  "latest_date": pd.to_datetime(combined["date"]).max().date().isoformat(),
 
1609
  dataset = dataset.drop(columns=["_session_date"])
1610
  dataset = dataset.sort_values("date").reset_index(drop=True)
1611
  dataset.to_parquet(OPENING_DATASET_PATH, index=False, compression="zstd")
 
1612
  latest = pd.to_datetime(dataset["date"], errors="coerce").max()
1613
  return {
1614
  "updated_rows": int(updated),
 
1616
  }
1617
 
1618
 
1619
+ def load_live_accuracy() -> dict[str, Any]:
1620
+ """Load the live accuracy ledger from disk."""
1621
+ default = {
1622
+ "tomorrow": {"entries": [], "accuracy": None, "total": 0, "correct_count": 0, "backtest_count": 0, "live_count": 0},
1623
+ "t5": {"entries": [], "accuracy": None, "total": 0, "correct_count": 0, "backtest_count": 0, "live_count": 0},
1624
+ "tplus1": {"entries": [], "accuracy": None, "total": 0, "correct_count": 0, "backtest_count": 0, "live_count": 0},
1625
+ }
1626
+
1627
+ if LIVE_ACCURACY_PATH.exists():
1628
+ try:
1629
+ raw = json.loads(LIVE_ACCURACY_PATH.read_text(encoding="utf-8"))
1630
+ except Exception:
1631
+ raw = None
1632
+ if isinstance(raw, dict):
1633
+ try:
1634
+ for model_id in default:
1635
+ current = raw.get(model_id, {})
1636
+ if not isinstance(current, dict):
1637
+ current = {}
1638
+ entries = current.get("entries", [])
1639
+ if not isinstance(entries, list):
1640
+ entries = []
1641
+ backtest_entries = [entry for entry in entries if str(entry.get("source", "backtest")).lower() == "backtest"]
1642
+ live_entries = [entry for entry in entries if str(entry.get("source", "backtest")).lower() != "backtest"]
1643
+ total = len(entries)
1644
+ correct = sum(1 for e in entries if e.get("correct"))
1645
+ current["entries"] = entries
1646
+ current["backtest_count"] = int(len(backtest_entries))
1647
+ current["live_count"] = int(len(live_entries))
1648
+ current["total"] = int(total)
1649
+ current["correct_count"] = int(correct)
1650
+ current["accuracy"] = (current["correct_count"] / current["total"]) if current["total"] > 0 else None
1651
+ default[model_id].update(current)
1652
+ return default
1653
+ except Exception:
1654
+ pass
1655
+ return default
1656
+
1657
+
1658
+ def save_live_accuracy(data: dict[str, Any]) -> None:
1659
+ """Persist the live accuracy ledger to disk."""
1660
+ LIVE_ACCURACY_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8")
1661
+
1662
+
1663
+ def _load_prediction_history(path: Path) -> pd.DataFrame:
1664
+ if not path.exists():
1665
+ return pd.DataFrame()
1666
+ frame = pd.read_parquet(path)
1667
+ for col in ("date", "input_date", "target_date", "forecast_date"):
1668
+ if col in frame.columns:
1669
+ frame[col] = pd.to_datetime(frame[col], errors="coerce")
1670
+ sort_cols = [col for col in ("target_date", "input_date", "date", "forecast_date") if col in frame.columns]
1671
+ if sort_cols:
1672
+ return frame.sort_values(sort_cols).reset_index(drop=True)
1673
+ return frame.reset_index(drop=True)
1674
+
1675
+
1676
+ def _record_prediction_history(path: Path, row: dict[str, Any], subset: list[str]) -> None:
1677
+ append_prediction_history(path, row, subset)
1678
+
1679
+
1680
+ def _rescore_tomorrow_live_ledger(ledger: dict[str, Any]) -> bool:
1681
+ """Fix live Tomorrow ledger entries to use close vs previous close. Returns True if modified."""
1682
+ daily = pd.read_parquet(NIFTY_1D_PATH)
1683
+ daily["_date"] = pd.to_datetime(daily["date"], errors="coerce").dt.normalize()
1684
+ closes_by_date: dict[date, float] = {}
1685
+ for _, row in daily.iterrows():
1686
+ if pd.isna(row["_date"]):
1687
+ continue
1688
+ close = row.get("close")
1689
+ if pd.notna(close) and np.isfinite(float(close)):
1690
+ closes_by_date[row["_date"].date()] = float(close)
1691
+
1692
+ changed = False
1693
+ for entry in ledger.get("tomorrow", {}).get("entries", []):
1694
+ if str(entry.get("source", "backtest")).lower() == "backtest":
1695
+ continue
1696
+ try:
1697
+ day = date.fromisoformat(str(entry.get("date", ""))[:10])
1698
+ except Exception:
1699
+ continue
1700
+ day_close = closes_by_date.get(day)
1701
+ if day_close is None:
1702
+ continue
1703
+ _, actual_direction = _tomorrow_actual_outcome(day, day_close, closes_by_date)
1704
+ if actual_direction is None:
1705
+ continue
1706
+ pred = str(entry.get("prediction", "")).upper()
1707
+ if pred not in {"UP", "DOWN"}:
1708
+ continue
1709
+ new_correct = pred == actual_direction
1710
+ if entry.get("actual") != actual_direction or entry.get("correct") != new_correct:
1711
+ entry["actual"] = actual_direction
1712
+ entry["correct"] = new_correct
1713
+ changed = True
1714
+ return changed
1715
+
1716
+
1717
+ def ensure_completed_sessions_scored(
1718
+ now: datetime | None = None,
1719
+ ledger: dict[str, Any] | None = None,
1720
+ ) -> dict[str, Any]:
1721
+ """Score any completed Tomorrow sessions that have daily close data but no ledger entry."""
1722
+ now = now or datetime.now(IST)
1723
+ completed = expected_completed_daily_date(now)
1724
+ if not is_trading_day(completed):
1725
+ return ledger or load_live_accuracy()
1726
+
1727
+ ledger = ledger or load_live_accuracy()
1728
+ if _rescore_tomorrow_live_ledger(ledger):
1729
+ for model_id in ("tomorrow",):
1730
+ entries = ledger[model_id]["entries"]
1731
+ total = len(entries)
1732
+ correct = sum(1 for e in entries if e.get("correct"))
1733
+ backtest_count = sum(1 for e in entries if str(e.get("source", "backtest")).lower() == "backtest")
1734
+ ledger[model_id]["total"] = total
1735
+ ledger[model_id]["correct_count"] = correct
1736
+ ledger[model_id]["backtest_count"] = backtest_count
1737
+ ledger[model_id]["live_count"] = total - backtest_count
1738
+ ledger[model_id]["accuracy"] = correct / total if total > 0 else None
1739
+ save_live_accuracy(ledger)
1740
+ logged_tom = {e["date"] for e in ledger.get("tomorrow", {}).get("entries", [])}
1741
+ if completed.isoformat() in logged_tom:
1742
+ return ledger
1743
+
1744
+ daily = pd.read_parquet(NIFTY_1D_PATH)
1745
+ daily["_date"] = pd.to_datetime(daily["date"], errors="coerce").dt.normalize()
1746
+ day_rows = daily[daily["_date"].dt.date == completed]
1747
+ if day_rows.empty:
1748
+ return ledger
1749
+
1750
+ day_open = float(day_rows.iloc[-1]["open"])
1751
+ day_close = float(day_rows.iloc[-1]["close"])
1752
+ if not (np.isfinite(day_open) and np.isfinite(day_close) and day_open != 0):
1753
+ return ledger
1754
+
1755
+ return update_live_accuracy(completed)
1756
+
1757
+
1758
+ def update_live_accuracy(session_date: date) -> dict[str, Any]:
1759
+ """Score today's predictions against actual outcomes and update the ledger.
1760
+
1761
+ Must be called AFTER refresh_daily_data() (so today's close is available)
1762
+ but BEFORE refresh_first5_prediction / refresh_tplus1_prediction /
1763
+ refresh_tomorrow_prediction (so the CSV files still hold the predictions
1764
+ we want to score).
1765
+ """
1766
+ ledger = load_live_accuracy()
1767
+ daily = pd.read_parquet(NIFTY_1D_PATH)
1768
+ daily["_date"] = pd.to_datetime(daily["date"], errors="coerce").dt.normalize()
1769
+ today_rows = daily[daily["_date"].dt.date == session_date]
1770
+ if today_rows.empty:
1771
+ return ledger
1772
+
1773
+ day_open = float(today_rows.iloc[-1]["open"])
1774
+ day_close = float(today_rows.iloc[-1]["close"])
1775
+ if not (np.isfinite(day_open) and np.isfinite(day_close) and day_open != 0):
1776
+ return ledger
1777
+ actual_close_gt_open = "UP" if day_close > day_open else "DOWN"
1778
+ session_iso = session_date.isoformat()
1779
+
1780
+ # --- T+5: today's 9:20 AM prediction vs close > open ---
1781
+ logged_t5 = {e["date"] for e in ledger["t5"]["entries"]}
1782
+ if session_iso not in logged_t5:
1783
+ t5_history = _load_prediction_history(T5_PREDICTION_HISTORY_PATH)
1784
+ if not t5_history.empty and "target_date" in t5_history.columns:
1785
+ t5_rows = t5_history[t5_history["target_date"].dt.date == session_date]
1786
+ else:
1787
+ t5_rows = pd.DataFrame()
1788
+ if t5_rows.empty and LATEST_PATH.exists():
1789
+ try:
1790
+ t5_row = pd.read_csv(LATEST_PATH).iloc[-1].to_dict()
1791
+ if str(t5_row.get("input_date", ""))[:10] == session_iso:
1792
+ t5_rows = pd.DataFrame([t5_row])
1793
+ except Exception:
1794
+ t5_rows = pd.DataFrame()
1795
+ if not t5_rows.empty:
1796
+ try:
1797
+ t5_row = t5_rows.iloc[-1].to_dict()
1798
+ pred = str(t5_row.get("prediction", "")).upper()
1799
+ if pred in ("UP", "DOWN"):
1800
+ ledger["t5"]["entries"].append({
1801
+ "date": session_iso,
1802
+ "prediction": pred,
1803
+ "actual": actual_close_gt_open,
1804
+ "correct": pred == actual_close_gt_open,
1805
+ "source": "live",
1806
+ })
1807
+ except Exception:
1808
+ pass
1809
+
1810
+ # --- Tomorrow: prior close vs today's close (matches forecaster target) ---
1811
+ logged_tom = {e["date"] for e in ledger["tomorrow"]["entries"]}
1812
+ if session_iso not in logged_tom:
1813
+ prev_day = previous_trading_day(session_date - timedelta(days=1))
1814
+ prev_rows = daily[daily["_date"].dt.date == prev_day]
1815
+ actual_tomorrow = None
1816
+ if not prev_rows.empty:
1817
+ prev_close = float(prev_rows.iloc[-1]["close"])
1818
+ if np.isfinite(prev_close) and prev_close != 0:
1819
+ actual_tomorrow = "UP" if day_close > prev_close else "DOWN"
1820
+ tom_row = _find_tomorrow_prediction_for_target(session_date)
1821
+ if tom_row and actual_tomorrow is not None:
1822
+ try:
1823
+ pred = str(tom_row.get("prediction", "")).upper()
1824
+ if pred in ("UP", "DOWN"):
1825
+ ledger["tomorrow"]["entries"].append({
1826
+ "date": session_iso,
1827
+ "prediction": pred,
1828
+ "actual": actual_tomorrow,
1829
+ "correct": pred == actual_tomorrow,
1830
+ "source": "live",
1831
+ })
1832
+ except Exception:
1833
+ pass
1834
+
1835
+ # --- T+1: yesterday's 14:20 prediction targeting today ---
1836
+ # T+1 target: today's close > yesterday's 14:20 close
1837
+ logged_t1 = {e["date"] for e in ledger["tplus1"]["entries"]}
1838
+ if session_iso not in logged_t1:
1839
+ t1_history = _load_prediction_history(TPLUS1_PREDICTION_HISTORY_PATH)
1840
+ if not t1_history.empty and "target_date" in t1_history.columns:
1841
+ t1_rows = t1_history[t1_history["target_date"].dt.date == session_date]
1842
+ else:
1843
+ t1_rows = pd.DataFrame()
1844
+ if not t1_rows.empty:
1845
+ try:
1846
+ t1_row = t1_rows.iloc[-1].to_dict()
1847
+ pred = str(t1_row.get("prediction", "")).upper()
1848
+ input_date_str = str(t1_row.get("input_date", ""))[:10]
1849
+ input_day = date.fromisoformat(input_date_str)
1850
+ # Read the 14:20 close from minute data for the input session
1851
+ minute = pd.read_parquet(NIFTY_1M_PATH, columns=["date", "close"])
1852
+ minute["dt"] = pd.to_datetime(minute["date"], errors="coerce")
1853
+ minute = minute.dropna(subset=["dt"])
1854
+ minute["session_date"] = minute["dt"].dt.normalize()
1855
+ minute["time_str"] = minute["dt"].dt.strftime("%H:%M")
1856
+ window = minute[
1857
+ (minute["session_date"].dt.date == input_day)
1858
+ & (minute["time_str"] >= "14:00")
1859
+ & (minute["time_str"] <= "14:20")
1860
+ ].sort_values("dt")
1861
+ if not window.empty and pred in ("UP", "DOWN"):
1862
+ w_close = float(window.iloc[-1]["close"])
1863
+ t1_actual = "UP" if day_close > w_close else "DOWN"
1864
+ ledger["tplus1"]["entries"].append({
1865
+ "date": session_iso,
1866
+ "prediction": pred,
1867
+ "actual": t1_actual,
1868
+ "correct": pred == t1_actual,
1869
+ "source": "live",
1870
+ })
1871
+ except Exception:
1872
+ pass
1873
+
1874
+ _rescore_tomorrow_live_ledger(ledger)
1875
+
1876
+ # Recompute summary stats
1877
+ for model_id in ("t5", "tomorrow", "tplus1"):
1878
+ entries = ledger[model_id]["entries"]
1879
+ total = len(entries)
1880
+ correct = sum(1 for e in entries if e.get("correct"))
1881
+ backtest_count = sum(1 for e in entries if str(e.get("source", "backtest")).lower() == "backtest")
1882
+ ledger[model_id]["total"] = total
1883
+ ledger[model_id]["correct_count"] = correct
1884
+ ledger[model_id]["backtest_count"] = backtest_count
1885
+ ledger[model_id]["live_count"] = total - backtest_count
1886
+ ledger[model_id]["accuracy"] = correct / total if total > 0 else None
1887
+
1888
+ save_live_accuracy(ledger)
1889
+ return ledger
1890
+
1891
+
1892
  def refresh_market_close_data(session_date: date | None = None) -> dict[str, Any]:
1893
  now = datetime.now(IST)
1894
  session_date = session_date or now.date()
1895
  if not is_trading_day(session_date):
1896
  raise RuntimeError(f"{session_date.isoformat()} is not an NSE trading session.")
 
1897
  try:
 
1898
  minutes = fetch_yahoo_minutes(period="7d")
1899
  minute_frame = append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
1900
  daily_info = refresh_daily_data()
 
1907
  tplus1_prediction = refresh_tplus1_prediction(session_date=session_date)
1908
  outcomes = update_opening_outcomes_from_daily()
1909
  tomorrow_prediction = refresh_tomorrow_prediction(session_date=session_date)
 
 
1910
  return {
1911
  "session_date": session_date.isoformat(),
1912
  "nifty_1m_rows": int(len(minute_frame)),
 
1916
  "t5_prediction": t5_prediction.to_dict(),
1917
  "tplus1_prediction": tplus1_prediction,
1918
  "tomorrow_prediction": tomorrow_prediction,
 
1919
  }
1920
+ except Exception:
 
 
1921
  raise
1922
 
1923
 
1924
+ def close_refresh_due(now: datetime | None = None) -> bool:
1925
  now = now or datetime.now(IST)
1926
  if not is_trading_day(now.date()) or now.time() < CLOSE_REFRESH_READY:
1927
  return False
 
1936
  tomorrow_input = date.fromisoformat(str(tomorrow_latest.get("input_date"))[:10])
1937
  except Exception:
1938
  tomorrow_input = None
1939
+ return any(
1940
+ latest != now.date()
1941
+ for latest in (latest_daily, latest_minutes, latest_opening, latest_opening_outcome, tomorrow_input)
1942
+ )
1943
+
1944
+
1945
+ def latest_prediction_input_date(path: Path) -> date | None:
1946
+ if not path.exists():
1947
+ return None
1948
+ try:
1949
+ frame = pd.read_csv(path, usecols=["input_date"])
1950
+ except Exception:
1951
+ return None
1952
+ if frame.empty:
1953
+ return None
1954
+ value = pd.to_datetime(frame["input_date"], errors="coerce").max()
1955
+ return None if pd.isna(value) else value.date()
1956
+
1957
+
1958
+ def latest_tomorrow_input_date() -> date | None:
1959
+ try:
1960
+ latest = latest_tomorrow_prediction()
1961
+ raw = latest.get("input_date")
1962
+ return date.fromisoformat(str(raw)[:10]) if raw else None
1963
+ except Exception:
1964
+ return None
1965
+
1966
+
1967
+ def expected_completed_daily_date(now: datetime | None = None) -> date:
1968
+ now = now or datetime.now(IST)
1969
+ if is_trading_day(now.date()) and now.time() < CLOSE_REFRESH_READY:
1970
+ return previous_trading_day(now.date() - timedelta(days=1))
1971
+ return previous_trading_day(now.date())
1972
+
1973
+
1974
+ def expected_minute_date(now: datetime | None = None) -> date:
1975
+ now = now or datetime.now(IST)
1976
+ if is_trading_day(now.date()) and now.time() >= FIRST5_READY:
1977
+ return now.date()
1978
+ return previous_trading_day(now.date() - timedelta(days=1))
1979
+
1980
+
1981
+ def expected_tplus1_date(now: datetime | None = None) -> date:
1982
+ now = now or datetime.now(IST)
1983
+ if is_trading_day(now.date()) and now.time() >= TPLUS1_READY:
1984
+ return now.date()
1985
+ return previous_trading_day(now.date() - timedelta(days=1))
1986
+
1987
+
1988
+ def is_stale(latest: date | None, expected: date) -> bool:
1989
+ return latest is None or latest < expected
1990
+
1991
+
1992
+ def stale_data_status(now: datetime | None = None) -> dict[str, Any]:
1993
+ now = now or datetime.now(IST)
1994
+ expected_daily = expected_completed_daily_date(now)
1995
+ expected_minutes = expected_minute_date(now)
1996
+ expected_tplus1 = expected_tplus1_date(now)
1997
+ latest_1d = latest_parquet_date(NIFTY_1D_PATH)
1998
+ latest_1m = latest_parquet_date(NIFTY_1M_PATH)
1999
+ latest_t5 = latest_prediction_input_date(LATEST_PATH)
2000
+ latest_tomorrow = latest_tomorrow_input_date()
2001
+ latest_tplus1 = latest_prediction_input_date(TPLUS1_LATEST_PATH)
2002
+ return {
2003
+ "daily_stale": expected_daily > (latest_1d or date.min),
2004
+ "minutes_stale": expected_minutes > (latest_1m or date.min),
2005
+ "t5_stale": is_stale(latest_t5, expected_minutes),
2006
+ "tomorrow_stale": is_stale(latest_tomorrow, expected_daily),
2007
+ "tplus1_stale": is_stale(latest_tplus1, expected_tplus1),
2008
+ }
2009
+
2010
+
2011
+ def refresh_stale_data_once(now: datetime | None = None) -> dict[str, Any]:
2012
+ now = now or datetime.now(IST)
2013
+ status = stale_data_status(now)
2014
+ if not any(status.values()):
2015
+ return {"status": "fresh", "actions": []}
2016
+ if not _stale_refresh_lock.acquire(blocking=False):
2017
+ return {"status": "skipped", "reason": "stale refresh already running", **status, "actions": []}
2018
+
2019
+ actions: list[dict[str, Any]] = []
2020
+ try:
2021
+ if status["minutes_stale"]:
2022
+ minutes = fetch_yahoo_minutes(period="7d")
2023
+ combined = append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
2024
+ actions.append(
2025
+ {
2026
+ "name": "minutes",
2027
+ "rows": int(len(combined)),
2028
+ "latest_date": pd.to_datetime(combined["date"], errors="coerce").max().date().isoformat(),
2029
+ }
2030
+ )
2031
+
2032
+ if status["daily_stale"]:
2033
+ daily_info = refresh_daily_data()
2034
+ outcomes = update_opening_outcomes_from_daily()
2035
+ actions.append({"name": "daily", **daily_info})
2036
+ actions.append({"name": "opening_outcomes", **outcomes})
2037
+ try:
2038
+ completed = date.fromisoformat(status["expected_daily_date"])
2039
+ scored = update_live_accuracy(completed)
2040
+ actions.append(
2041
+ {
2042
+ "name": "tomorrow_live_accuracy",
2043
+ "session_date": completed.isoformat(),
2044
+ "live_count": scored.get("tomorrow", {}).get("live_count"),
2045
+ }
2046
+ )
2047
+ except Exception as exc:
2048
+ actions.append({"name": "tomorrow_live_accuracy", "error": str(exc)})
2049
+
2050
+ if status["daily_stale"] or status["tomorrow_stale"]:
2051
+ try:
2052
+ tomorrow = refresh_tomorrow_prediction(session_date=date.fromisoformat(status["expected_daily_date"]))
2053
+ actions.append({"name": "tomorrow_prediction", "input_date": tomorrow.get("input_date")})
2054
+ except Exception as exc:
2055
+ actions.append({"name": "tomorrow_prediction", "error": str(exc)})
2056
+
2057
+ if status["t5_stale"] and is_trading_day(now.date()) and now.time() >= FIRST5_READY:
2058
+ prediction = refresh_first5_prediction(session_date=now.date())
2059
+ actions.append({"name": "t5_prediction", "input_date": prediction.input_date})
2060
+
2061
+ if status["tplus1_stale"] and is_trading_day(now.date()) and now.time() >= TPLUS1_READY:
2062
+ prediction = refresh_tplus1_prediction(session_date=now.date())
2063
+ actions.append({"name": "tplus1_prediction", "input_date": prediction.get("input_date")})
2064
+
2065
+ clear_dashboard_payload_cache()
2066
+ refreshed_status = stale_data_status(datetime.now(IST))
2067
+ return {"status": "refreshed", **refreshed_status, "actions": actions}
2068
+ finally:
2069
+ _stale_refresh_lock.release()
2070
+
2071
+
2072
+ def next_ist_run_at(run_time: time = time(9, 20), now: datetime | None = None) -> datetime:
2073
  now = now or datetime.now(IST)
2074
  target_day = now.date()
2075
  if now >= datetime.combine(target_day, run_time, tzinfo=IST):