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

Add Almanac accuracy and snapshot APIs

Browse files
app.py CHANGED
@@ -28,6 +28,7 @@ try:
28
  RISK_HORIZON_MAP,
29
  RISK_HORIZON_LABELS,
30
  derive_investment_signal,
 
31
  )
32
  iris_app = IRIS_System()
33
  except ImportError as e:
@@ -111,6 +112,10 @@ SECTOR_PEERS = {
111
  _yf_info_cache = {}
112
  _YF_INFO_TTL = 300 # seconds
113
  _almanac_data = None
 
 
 
 
114
 
115
  _ALMANAC_INDEX_KEY_MAP = {
116
  "djia": "dow",
@@ -331,6 +336,161 @@ def _load_almanac_data():
331
  return _almanac_data
332
 
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  def _get_related_tickers(ticker, count=7):
335
  """Return a list of related tickers based on the sector of the given ticker."""
336
  fallback = ["AAPL", "MSFT", "GOOG", "AMZN", "NVDA", "META", "TSLA"]
@@ -576,6 +736,254 @@ def almanac_week():
576
  }
577
  )
578
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  @app.route('/api/history/<ticker>', methods=['GET'])
580
  def get_history(ticker):
581
  """Return lightweight market history points directly from yfinance for chart rendering."""
 
28
  RISK_HORIZON_MAP,
29
  RISK_HORIZON_LABELS,
30
  derive_investment_signal,
31
+ generate_rf_reasoning,
32
  )
33
  iris_app = IRIS_System()
34
  except ImportError as e:
 
112
  _yf_info_cache = {}
113
  _YF_INFO_TTL = 300 # seconds
114
  _almanac_data = None
115
+ _accuracy_data = None
116
+ _accuracy_mtime = 0.0
117
+ _iris_snapshot_cache = {"data": None, "ts": 0.0}
118
+ _IRIS_SNAPSHOT_TTL = 300 # 5 minutes
119
 
120
  _ALMANAC_INDEX_KEY_MAP = {
121
  "djia": "dow",
 
336
  return _almanac_data
337
 
338
 
339
+ def _load_accuracy_data():
340
+ """Load accuracy_results.json with file-mtime caching."""
341
+ global _accuracy_data, _accuracy_mtime
342
+ path = PROJECT_ROOT / "data" / "almanac_2026" / "accuracy_results.json"
343
+ if not path.exists():
344
+ return None
345
+ mtime = path.stat().st_mtime
346
+ if _accuracy_data is not None and mtime <= _accuracy_mtime:
347
+ return _accuracy_data
348
+ try:
349
+ with open(path, "r", encoding="utf-8") as f:
350
+ _accuracy_data = json.load(f)
351
+ _accuracy_mtime = mtime
352
+ return _accuracy_data
353
+ except Exception as e:
354
+ print(f"[ACCURACY] Error loading: {e}")
355
+ return None
356
+
357
+
358
+ def _iris_price_threshold(symbol: str) -> float:
359
+ token = str(symbol or "").strip().upper()
360
+ if "DJI" in token:
361
+ return 10000.0
362
+ if "IXIC" in token:
363
+ return 5000.0
364
+ return 400.0
365
+
366
+
367
+ def _safe_float(value, default=0.0):
368
+ try:
369
+ if value is None or value == "":
370
+ return float(default)
371
+ return float(value)
372
+ except (TypeError, ValueError):
373
+ return float(default)
374
+
375
+
376
+ def _iris_direction_from_pct_change(pct_change: float) -> str:
377
+ if pct_change > 0:
378
+ return "upward"
379
+ if pct_change < 0:
380
+ return "downward"
381
+ return "flat"
382
+
383
+
384
+ def _iris_prediction_light(trend_label: str, sentiment_score=0.0) -> str:
385
+ normalized_trend = str(trend_label or "").upper()
386
+ sentiment = _safe_float(sentiment_score, 0.0)
387
+ if sentiment < -0.05 or "STRONG DOWNTREND" in normalized_trend:
388
+ return " RED (Risk Detected - Caution)"
389
+ if abs(sentiment) < 0.05 and "WEAK" in normalized_trend:
390
+ return " YELLOW (Neutral / Noise)"
391
+ return " GREEN (Safe to Proceed)"
392
+
393
+
394
+ def _read_latest_iris_report(symbol: str):
395
+ """Read the latest valid IRIS report for the requested symbol from DATA_DIR."""
396
+ token = str(symbol or "").strip().upper()
397
+ bare = token.lstrip("^_")
398
+ filename_candidates = []
399
+ for candidate in (
400
+ f"{token}_report.json",
401
+ f"^{bare}_report.json",
402
+ f"_{bare}_report.json",
403
+ f"{bare}_report.json",
404
+ ):
405
+ path = DATA_DIR / candidate
406
+ if path not in filename_candidates:
407
+ filename_candidates.append(path)
408
+
409
+ min_price = _iris_price_threshold(token)
410
+
411
+ for path in filename_candidates:
412
+ if not path.exists():
413
+ continue
414
+ try:
415
+ with open(path, "r", encoding="utf-8") as f:
416
+ reports = json.load(f)
417
+ if not isinstance(reports, list):
418
+ reports = [reports]
419
+ for report in reversed(reports):
420
+ if not isinstance(report, dict):
421
+ continue
422
+ current_price = _safe_float(report.get("market", {}).get("current_price"), 0.0)
423
+ if current_price < min_price:
424
+ continue
425
+ horizon_1d = report.get("all_horizons", {}).get("1D", {})
426
+ meta = report.get("meta", {})
427
+ horizon_days = meta.get("horizon_days", 1) if isinstance(meta, dict) else 1
428
+ if not isinstance(horizon_1d, dict) and int(_safe_float(horizon_days, 1)) != 1:
429
+ continue
430
+ return report
431
+ except Exception:
432
+ continue
433
+ return None
434
+
435
+
436
+ def _format_iris_snapshot_entry(report: dict, label: str):
437
+ meta = report.get("meta", {}) if isinstance(report, dict) else {}
438
+ market = report.get("market", {}) if isinstance(report, dict) else {}
439
+ signals = report.get("signals", {}) if isinstance(report, dict) else {}
440
+ h1d = report.get("all_horizons", {}).get("1D", {}) if isinstance(report, dict) else {}
441
+ if not isinstance(meta, dict):
442
+ meta = {}
443
+ if not isinstance(market, dict):
444
+ market = {}
445
+ if not isinstance(signals, dict):
446
+ signals = {}
447
+ if not isinstance(h1d, dict):
448
+ h1d = {}
449
+
450
+ current_price = _safe_float(market.get("current_price"), 0.0)
451
+ predicted_price = (
452
+ market.get("predicted_price_next_session")
453
+ or h1d.get("predicted_price")
454
+ or market.get("predicted_price_horizon")
455
+ )
456
+ predicted_price = _safe_float(predicted_price, 0.0)
457
+
458
+ reasoning = h1d.get("iris_reasoning") or signals.get("iris_reasoning") or {}
459
+ if not isinstance(reasoning, dict):
460
+ reasoning = {}
461
+ pct_change = reasoning.get("pct_change")
462
+ if pct_change in (None, "") and current_price:
463
+ pct_change = ((predicted_price - current_price) / current_price) * 100
464
+ pct_change = round(_safe_float(pct_change, 0.0), 2)
465
+
466
+ direction = str(reasoning.get("direction", "")).strip().lower()
467
+ if not direction:
468
+ direction = _iris_direction_from_pct_change(pct_change)
469
+
470
+ top_factors = reasoning.get("top_factors", [])
471
+ if not isinstance(top_factors, list):
472
+ top_factors = []
473
+
474
+ return {
475
+ "available": True,
476
+ "label": label,
477
+ "symbol": str(meta.get("symbol") or meta.get("source_symbol") or "").strip(),
478
+ "session_date": str(meta.get("market_session_date", "")).strip(),
479
+ "generated_at": str(meta.get("generated_at", "")).strip(),
480
+ "current_price": current_price or None,
481
+ "predicted_price": predicted_price or None,
482
+ "trend_label": str(h1d.get("trend_label") or signals.get("trend_label", "")).strip(),
483
+ "investment_signal": str(h1d.get("investment_signal") or signals.get("investment_signal", "")).strip(),
484
+ "check_engine_light": str(signals.get("check_engine_light", "")).strip(),
485
+ "pct_change": pct_change,
486
+ "direction": direction,
487
+ "top_factors": top_factors,
488
+ "model_confidence": h1d.get("model_confidence") or signals.get("model_confidence"),
489
+ "sentiment_score": _safe_float(signals.get("sentiment_score"), 0.0),
490
+ "source": "report_snapshot",
491
+ }
492
+
493
+
494
  def _get_related_tickers(ticker, count=7):
495
  """Return a list of related tickers based on the sector of the given ticker."""
496
  fallback = ["AAPL", "MSFT", "GOOG", "AMZN", "NVDA", "META", "TSLA"]
 
736
  }
737
  )
738
 
739
+
740
+ # --- Almanac Accuracy Tracking API ---
741
+
742
+ def _accuracy_unavailable_response():
743
+ return jsonify(
744
+ {
745
+ "available": False,
746
+ "message": "Run scripts/seed_accuracy.py to generate accuracy data.",
747
+ }
748
+ )
749
+
750
+
751
+ def _accuracy_pct(hits, total):
752
+ if not total:
753
+ return 0.0
754
+ return round((hits / total) * 100, 1)
755
+
756
+
757
+ @app.route('/api/almanac/accuracy')
758
+ def almanac_accuracy():
759
+ """Return almanac historic accuracy results."""
760
+ data = _load_accuracy_data()
761
+ if data is None:
762
+ return _accuracy_unavailable_response()
763
+
764
+ daily = data.get("daily", {})
765
+ date_param = str(request.args.get("date", "") or "").strip()
766
+ from_param = str(request.args.get("from", "") or "").strip()
767
+ to_param = str(request.args.get("to", "") or "").strip()
768
+
769
+ if date_param:
770
+ entry = daily.get(date_param)
771
+ if entry is None:
772
+ return jsonify({"error": f"No accuracy data for {date_param}"}), 404
773
+ return jsonify(entry)
774
+
775
+ if from_param and to_param:
776
+ filtered = {k: v for k, v in daily.items() if from_param <= k <= to_param}
777
+ return jsonify({"from": from_param, "to": to_param, "daily": filtered})
778
+
779
+ return jsonify({"daily": daily})
780
+
781
+
782
+ @app.route('/api/almanac/accuracy/week')
783
+ def almanac_accuracy_week():
784
+ """Return weekly accuracy results for the requested week."""
785
+ data = _load_accuracy_data()
786
+ if data is None:
787
+ return _accuracy_unavailable_response()
788
+
789
+ start = str(request.args.get("start", "") or "").strip()
790
+ if not start:
791
+ return jsonify({"error": "start query parameter is required"}), 400
792
+
793
+ try:
794
+ week_key = datetime.strptime(start, "%Y-%m-%d").strftime("%Y-W%W")
795
+ except ValueError:
796
+ return jsonify({"error": "Invalid start date. Expected YYYY-MM-DD"}), 400
797
+
798
+ weekly_entry = (data.get("weekly") or {}).get(week_key)
799
+ if weekly_entry is None:
800
+ return jsonify({"error": f"No weekly accuracy found for {week_key}"}), 404
801
+ return jsonify(weekly_entry)
802
+
803
+
804
+ @app.route('/api/almanac/accuracy/month')
805
+ def almanac_accuracy_month():
806
+ """Return monthly accuracy results for the requested month."""
807
+ data = _load_accuracy_data()
808
+ if data is None:
809
+ return _accuracy_unavailable_response()
810
+
811
+ month_key = str(request.args.get("month", "") or "").strip()
812
+ if not month_key:
813
+ return jsonify({"error": "month query parameter is required"}), 400
814
+
815
+ monthly_entry = (data.get("monthly") or {}).get(month_key)
816
+ if monthly_entry is None:
817
+ return jsonify({"error": f"No monthly accuracy found for {month_key}"}), 404
818
+ return jsonify(monthly_entry)
819
+
820
+
821
+ @app.route('/api/almanac/accuracy/summary')
822
+ def almanac_accuracy_summary():
823
+ """Return aggregate historic accuracy metrics."""
824
+ data = _load_accuracy_data()
825
+ if data is None:
826
+ return _accuracy_unavailable_response()
827
+
828
+ monthly = data.get("monthly") or {}
829
+ daily = data.get("daily") or {}
830
+
831
+ overall_hits = sum(int(month.get("hits", 0)) for month in monthly.values())
832
+ overall_total = sum(int(month.get("total_calls", 0)) for month in monthly.values())
833
+
834
+ per_index = {}
835
+ for index_key in ("dow", "sp500", "nasdaq"):
836
+ hits = sum(int(month.get(index_key, {}).get("hits", 0)) for month in monthly.values())
837
+ total = sum(int(month.get(index_key, {}).get("total", 0)) for month in monthly.values())
838
+ per_index[index_key] = {
839
+ "hits": hits,
840
+ "total": total,
841
+ "pct": _accuracy_pct(hits, total),
842
+ }
843
+
844
+ return jsonify(
845
+ {
846
+ "overall": {
847
+ "hits": overall_hits,
848
+ "total_calls": overall_total,
849
+ "accuracy": _accuracy_pct(overall_hits, overall_total),
850
+ },
851
+ "monthly": monthly,
852
+ "per_index": per_index,
853
+ "last_scored_date": max(daily.keys()) if daily else None,
854
+ "total_days": len(daily),
855
+ }
856
+ )
857
+
858
+
859
+ # --- IRIS Snapshot for Almanac Dashboard ---
860
+
861
+ @app.route('/api/almanac/iris-snapshot')
862
+ def almanac_iris_snapshot():
863
+ """Return the latest cached IRIS index predictions from on-disk report files."""
864
+ now = time.time()
865
+ if (
866
+ _iris_snapshot_cache["data"] is not None
867
+ and (now - _iris_snapshot_cache["ts"]) < _IRIS_SNAPSHOT_TTL
868
+ ):
869
+ return jsonify(_iris_snapshot_cache["data"])
870
+
871
+ symbols = {
872
+ "spy": {"file_symbol": "SPY", "label": "SPY (S&P 500 ETF)"},
873
+ "dji": {"file_symbol": "^DJI", "label": "Dow Jones"},
874
+ "gspc": {"file_symbol": "^GSPC", "label": "S&P 500 Index"},
875
+ "ixic": {"file_symbol": "^IXIC", "label": "NASDAQ"},
876
+ }
877
+
878
+ result = {
879
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
880
+ "indices": {},
881
+ }
882
+
883
+ for key, info in symbols.items():
884
+ report = _read_latest_iris_report(info["file_symbol"])
885
+ if not report:
886
+ result["indices"][key] = {
887
+ "available": False,
888
+ "label": info["label"],
889
+ }
890
+ continue
891
+ result["indices"][key] = _format_iris_snapshot_entry(report, info["label"])
892
+
893
+ _iris_snapshot_cache["data"] = result
894
+ _iris_snapshot_cache["ts"] = now
895
+ return jsonify(result)
896
+
897
+
898
+ @app.route('/api/almanac/iris-refresh')
899
+ def almanac_iris_refresh():
900
+ """Run lightweight 1D IRIS predictions for the dashboard's major indices."""
901
+ if not iris_app:
902
+ return jsonify({"error": "IRIS not initialized"}), 500
903
+
904
+ tickers = {
905
+ "spy": {"ticker": "SPY", "label": "SPY (S&P 500 ETF)"},
906
+ "dji": {"ticker": "^DJI", "label": "Dow Jones"},
907
+ "gspc": {"ticker": "^GSPC", "label": "S&P 500 Index"},
908
+ "ixic": {"ticker": "^IXIC", "label": "NASDAQ"},
909
+ }
910
+ generated_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
911
+ result = {"generated_at": generated_at, "indices": {}}
912
+
913
+ for key, info in tickers.items():
914
+ ticker = info["ticker"]
915
+ try:
916
+ data = iris_app.get_market_data(ticker)
917
+ if not data:
918
+ result["indices"][key] = {
919
+ "available": False,
920
+ "label": info["label"],
921
+ }
922
+ continue
923
+
924
+ trend_label, predicted_price, trajectory, traj_upper, traj_lower, rf_model, model_confidence = iris_app.predict_trend(
925
+ data,
926
+ sentiment_score=0.0,
927
+ horizon_days=1,
928
+ )
929
+
930
+ current_price = _safe_float(data.get("current_price"), 0.0)
931
+ pct_change = ((predicted_price - current_price) / current_price * 100) if current_price else 0.0
932
+ history_df = data.get("history_df")
933
+ last_rsi = 50.0
934
+ session_date = time.strftime("%Y-%m-%d")
935
+ if history_df is not None and "rsi_14" in history_df.columns and len(history_df):
936
+ last_rsi = float(history_df["rsi_14"].iloc[-1])
937
+ if history_df is not None and len(history_df):
938
+ try:
939
+ session_date = str(pd.Timestamp(history_df.index[-1]).date())
940
+ except Exception:
941
+ session_date = time.strftime("%Y-%m-%d")
942
+
943
+ investment_signal = derive_investment_signal(pct_change, 0.0, last_rsi, 1)
944
+ reasoning = {}
945
+ if rf_model is not None:
946
+ try:
947
+ reasoning = generate_rf_reasoning(
948
+ rf_model,
949
+ None,
950
+ current_price,
951
+ predicted_price,
952
+ "1 Day",
953
+ )
954
+ except Exception:
955
+ reasoning = {}
956
+
957
+ result["indices"][key] = {
958
+ "available": True,
959
+ "label": info["label"],
960
+ "symbol": ticker,
961
+ "session_date": session_date,
962
+ "generated_at": generated_at,
963
+ "current_price": round(current_price, 6),
964
+ "predicted_price": round(float(predicted_price), 6),
965
+ "trend_label": trend_label,
966
+ "investment_signal": investment_signal,
967
+ "check_engine_light": _iris_prediction_light(trend_label, 0.0).strip(),
968
+ "pct_change": round(float(pct_change), 2),
969
+ "direction": _iris_direction_from_pct_change(pct_change),
970
+ "top_factors": reasoning.get("top_factors", []) if isinstance(reasoning, dict) else [],
971
+ "model_confidence": round(float(model_confidence), 1),
972
+ "sentiment_score": 0.0,
973
+ "source": "live_rf_prediction",
974
+ }
975
+ except Exception as e:
976
+ result["indices"][key] = {
977
+ "available": False,
978
+ "label": info["label"],
979
+ "error": str(e),
980
+ }
981
+
982
+ _iris_snapshot_cache["data"] = None
983
+ _iris_snapshot_cache["ts"] = 0.0
984
+ return jsonify(result)
985
+
986
+
987
  @app.route('/api/history/<ticker>', methods=['GET'])
988
  def get_history(ticker):
989
  """Return lightweight market history points directly from yfinance for chart rendering."""
data/almanac_2026/accuracy_results.json ADDED
@@ -0,0 +1,3118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "meta": {
3
+ "last_updated": "2026-04-05T07:26:51.578106Z",
4
+ "total_days_scored": 63,
5
+ "data_range": {
6
+ "from": "2026-01-02",
7
+ "to": "2026-04-02"
8
+ },
9
+ "source": "Historic CSV backtest via scripts/seed_accuracy.py"
10
+ },
11
+ "daily": {
12
+ "2026-01-02": {
13
+ "actual": {
14
+ "dji": 48382.39,
15
+ "sp500": 6858.47,
16
+ "nasdaq": 23235.63
17
+ },
18
+ "prev_close": {
19
+ "dji": 48063.29,
20
+ "sp500": 6845.5,
21
+ "nasdaq": 23241.99
22
+ },
23
+ "pct_change": {
24
+ "dji": 0.006639,
25
+ "sp500": 0.001895,
26
+ "nasdaq": -0.000274
27
+ },
28
+ "almanac_scores": {
29
+ "d": 66.7,
30
+ "s": 52.4,
31
+ "n": 61.9
32
+ },
33
+ "results": {
34
+ "d": {
35
+ "verdict": "HIT",
36
+ "predicted": "UP",
37
+ "actual": "UP"
38
+ },
39
+ "s": {
40
+ "verdict": "HIT",
41
+ "predicted": "UP",
42
+ "actual": "UP"
43
+ },
44
+ "n": {
45
+ "verdict": "MISS",
46
+ "predicted": "UP",
47
+ "actual": "DOWN"
48
+ }
49
+ },
50
+ "hits": 2,
51
+ "total_calls": 3,
52
+ "context": "First Trading Day of Year, NASDAQ Up 18 of Last 28"
53
+ },
54
+ "2026-01-05": {
55
+ "actual": {
56
+ "dji": 48977.18,
57
+ "sp500": 6902.05,
58
+ "nasdaq": 23395.82
59
+ },
60
+ "prev_close": {
61
+ "dji": 48382.39,
62
+ "sp500": 6858.47,
63
+ "nasdaq": 23235.63
64
+ },
65
+ "pct_change": {
66
+ "dji": 0.012294,
67
+ "sp500": 0.006354,
68
+ "nasdaq": 0.006894
69
+ },
70
+ "almanac_scores": {
71
+ "d": 61.9,
72
+ "s": 47.6,
73
+ "n": 38.1
74
+ },
75
+ "results": {
76
+ "d": {
77
+ "verdict": "HIT",
78
+ "predicted": "UP",
79
+ "actual": "UP"
80
+ },
81
+ "s": {
82
+ "verdict": "MISS",
83
+ "predicted": "DOWN",
84
+ "actual": "UP"
85
+ },
86
+ "n": {
87
+ "verdict": "MISS",
88
+ "predicted": "DOWN",
89
+ "actual": "UP"
90
+ }
91
+ },
92
+ "hits": 1,
93
+ "total_calls": 3,
94
+ "context": "Second Trading Day of the Year, Dow Up 21 of Last 32; Santa Claus Rally Ends"
95
+ },
96
+ "2026-01-06": {
97
+ "actual": {
98
+ "dji": 49462.08,
99
+ "sp500": 6944.82,
100
+ "nasdaq": 23547.17
101
+ },
102
+ "prev_close": {
103
+ "dji": 48977.18,
104
+ "sp500": 6902.05,
105
+ "nasdaq": 23395.82
106
+ },
107
+ "pct_change": {
108
+ "dji": 0.009901,
109
+ "sp500": 0.006197,
110
+ "nasdaq": 0.006469
111
+ },
112
+ "almanac_scores": {
113
+ "d": 47.6,
114
+ "s": 52.4,
115
+ "n": 47.6
116
+ },
117
+ "results": {
118
+ "d": {
119
+ "verdict": "MISS",
120
+ "predicted": "DOWN",
121
+ "actual": "UP"
122
+ },
123
+ "s": {
124
+ "verdict": "HIT",
125
+ "predicted": "UP",
126
+ "actual": "UP"
127
+ },
128
+ "n": {
129
+ "verdict": "MISS",
130
+ "predicted": "DOWN",
131
+ "actual": "UP"
132
+ }
133
+ },
134
+ "hits": 1,
135
+ "total_calls": 3,
136
+ "context": ""
137
+ },
138
+ "2026-01-07": {
139
+ "actual": {
140
+ "dji": 48996.08,
141
+ "sp500": 6920.93,
142
+ "nasdaq": 23584.27
143
+ },
144
+ "prev_close": {
145
+ "dji": 49462.08,
146
+ "sp500": 6944.82,
147
+ "nasdaq": 23547.17
148
+ },
149
+ "pct_change": {
150
+ "dji": -0.009421,
151
+ "sp500": -0.00344,
152
+ "nasdaq": 0.001576
153
+ },
154
+ "almanac_scores": {
155
+ "d": 61.9,
156
+ "s": 66.7,
157
+ "n": 61.9
158
+ },
159
+ "results": {
160
+ "d": {
161
+ "verdict": "MISS",
162
+ "predicted": "UP",
163
+ "actual": "DOWN"
164
+ },
165
+ "s": {
166
+ "verdict": "MISS",
167
+ "predicted": "UP",
168
+ "actual": "DOWN"
169
+ },
170
+ "n": {
171
+ "verdict": "HIT",
172
+ "predicted": "UP",
173
+ "actual": "UP"
174
+ }
175
+ },
176
+ "hits": 1,
177
+ "total_calls": 3,
178
+ "context": ""
179
+ },
180
+ "2026-01-08": {
181
+ "actual": {
182
+ "dji": 49266.11,
183
+ "sp500": 6921.46,
184
+ "nasdaq": 23480.02
185
+ },
186
+ "prev_close": {
187
+ "dji": 48996.08,
188
+ "sp500": 6920.93,
189
+ "nasdaq": 23584.27
190
+ },
191
+ "pct_change": {
192
+ "dji": 0.005511,
193
+ "sp500": 7.7e-05,
194
+ "nasdaq": -0.00442
195
+ },
196
+ "almanac_scores": {
197
+ "d": 42.9,
198
+ "s": 52.4,
199
+ "n": 71.4
200
+ },
201
+ "results": {
202
+ "d": {
203
+ "verdict": "MISS",
204
+ "predicted": "DOWN",
205
+ "actual": "UP"
206
+ },
207
+ "s": {
208
+ "verdict": "HIT",
209
+ "predicted": "UP",
210
+ "actual": "UP"
211
+ },
212
+ "n": {
213
+ "verdict": "MISS",
214
+ "predicted": "UP",
215
+ "actual": "DOWN"
216
+ }
217
+ },
218
+ "hits": 1,
219
+ "total_calls": 3,
220
+ "context": "January's First Five Days Act as an Early Warning"
221
+ },
222
+ "2026-01-09": {
223
+ "actual": {
224
+ "dji": 49504.07,
225
+ "sp500": 6966.28,
226
+ "nasdaq": 23671.35
227
+ },
228
+ "prev_close": {
229
+ "dji": 49266.11,
230
+ "sp500": 6921.46,
231
+ "nasdaq": 23480.02
232
+ },
233
+ "pct_change": {
234
+ "dji": 0.00483,
235
+ "sp500": 0.006476,
236
+ "nasdaq": 0.008149
237
+ },
238
+ "almanac_scores": {
239
+ "d": 52.4,
240
+ "s": 57.1,
241
+ "n": 66.7
242
+ },
243
+ "results": {
244
+ "d": {
245
+ "verdict": "HIT",
246
+ "predicted": "UP",
247
+ "actual": "UP"
248
+ },
249
+ "s": {
250
+ "verdict": "HIT",
251
+ "predicted": "UP",
252
+ "actual": "UP"
253
+ },
254
+ "n": {
255
+ "verdict": "HIT",
256
+ "predicted": "UP",
257
+ "actual": "UP"
258
+ }
259
+ },
260
+ "hits": 3,
261
+ "total_calls": 3,
262
+ "context": "January Ends Best Three-Month Span"
263
+ },
264
+ "2026-01-12": {
265
+ "actual": {
266
+ "dji": 49590.2,
267
+ "sp500": 6977.27,
268
+ "nasdaq": 23733.9
269
+ },
270
+ "prev_close": {
271
+ "dji": 49504.07,
272
+ "sp500": 6966.28,
273
+ "nasdaq": 23671.35
274
+ },
275
+ "pct_change": {
276
+ "dji": 0.00174,
277
+ "sp500": 0.001578,
278
+ "nasdaq": 0.002642
279
+ },
280
+ "almanac_scores": {
281
+ "d": 61.9,
282
+ "s": 71.4,
283
+ "n": 71.4
284
+ },
285
+ "results": {
286
+ "d": {
287
+ "verdict": "HIT",
288
+ "predicted": "UP",
289
+ "actual": "UP"
290
+ },
291
+ "s": {
292
+ "verdict": "HIT",
293
+ "predicted": "UP",
294
+ "actual": "UP"
295
+ },
296
+ "n": {
297
+ "verdict": "HIT",
298
+ "predicted": "UP",
299
+ "actual": "UP"
300
+ }
301
+ },
302
+ "hits": 3,
303
+ "total_calls": 3,
304
+ "context": "First Trading Day of January Monthly Expiration Week, Dow Up 20 of Last 33"
305
+ },
306
+ "2026-01-13": {
307
+ "actual": {
308
+ "dji": 49191.99,
309
+ "sp500": 6963.74,
310
+ "nasdaq": 23709.87
311
+ },
312
+ "prev_close": {
313
+ "dji": 49590.2,
314
+ "sp500": 6977.27,
315
+ "nasdaq": 23733.9
316
+ },
317
+ "pct_change": {
318
+ "dji": -0.00803,
319
+ "sp500": -0.001939,
320
+ "nasdaq": -0.001012
321
+ },
322
+ "almanac_scores": {
323
+ "d": 52.4,
324
+ "s": 52.4,
325
+ "n": 61.9
326
+ },
327
+ "results": {
328
+ "d": {
329
+ "verdict": "MISS",
330
+ "predicted": "UP",
331
+ "actual": "DOWN"
332
+ },
333
+ "s": {
334
+ "verdict": "MISS",
335
+ "predicted": "UP",
336
+ "actual": "DOWN"
337
+ },
338
+ "n": {
339
+ "verdict": "MISS",
340
+ "predicted": "UP",
341
+ "actual": "DOWN"
342
+ }
343
+ },
344
+ "hits": 0,
345
+ "total_calls": 3,
346
+ "context": ""
347
+ },
348
+ "2026-01-14": {
349
+ "actual": {
350
+ "dji": 49149.63,
351
+ "sp500": 6926.6,
352
+ "nasdaq": 23471.75
353
+ },
354
+ "prev_close": {
355
+ "dji": 49191.99,
356
+ "sp500": 6963.74,
357
+ "nasdaq": 23709.87
358
+ },
359
+ "pct_change": {
360
+ "dji": -0.000861,
361
+ "sp500": -0.005333,
362
+ "nasdaq": -0.010043
363
+ },
364
+ "almanac_scores": {
365
+ "d": 47.6,
366
+ "s": 52.4,
367
+ "n": 47.6
368
+ },
369
+ "results": {
370
+ "d": {
371
+ "verdict": "HIT",
372
+ "predicted": "DOWN",
373
+ "actual": "DOWN"
374
+ },
375
+ "s": {
376
+ "verdict": "MISS",
377
+ "predicted": "UP",
378
+ "actual": "DOWN"
379
+ },
380
+ "n": {
381
+ "verdict": "HIT",
382
+ "predicted": "DOWN",
383
+ "actual": "DOWN"
384
+ }
385
+ },
386
+ "hits": 2,
387
+ "total_calls": 3,
388
+ "context": "January Monthly Expiration Week, Dow Up 11 of Last 15"
389
+ },
390
+ "2026-01-15": {
391
+ "actual": {
392
+ "dji": 49442.44,
393
+ "sp500": 6944.47,
394
+ "nasdaq": 23530.02
395
+ },
396
+ "prev_close": {
397
+ "dji": 49149.63,
398
+ "sp500": 6926.6,
399
+ "nasdaq": 23471.75
400
+ },
401
+ "pct_change": {
402
+ "dji": 0.005958,
403
+ "sp500": 0.00258,
404
+ "nasdaq": 0.002483
405
+ },
406
+ "almanac_scores": {
407
+ "d": 42.9,
408
+ "s": 47.6,
409
+ "n": 42.9
410
+ },
411
+ "results": {
412
+ "d": {
413
+ "verdict": "MISS",
414
+ "predicted": "DOWN",
415
+ "actual": "UP"
416
+ },
417
+ "s": {
418
+ "verdict": "MISS",
419
+ "predicted": "DOWN",
420
+ "actual": "UP"
421
+ },
422
+ "n": {
423
+ "verdict": "MISS",
424
+ "predicted": "DOWN",
425
+ "actual": "UP"
426
+ }
427
+ },
428
+ "hits": 0,
429
+ "total_calls": 3,
430
+ "context": ""
431
+ },
432
+ "2026-01-16": {
433
+ "actual": {
434
+ "dji": 49359.33,
435
+ "sp500": 6940.01,
436
+ "nasdaq": 23515.39
437
+ },
438
+ "prev_close": {
439
+ "dji": 49442.44,
440
+ "sp500": 6944.47,
441
+ "nasdaq": 23530.02
442
+ },
443
+ "pct_change": {
444
+ "dji": -0.001681,
445
+ "sp500": -0.000642,
446
+ "nasdaq": -0.000622
447
+ },
448
+ "almanac_scores": {
449
+ "d": 57.1,
450
+ "s": 66.7,
451
+ "n": 66.7
452
+ },
453
+ "results": {
454
+ "d": {
455
+ "verdict": "MISS",
456
+ "predicted": "UP",
457
+ "actual": "DOWN"
458
+ },
459
+ "s": {
460
+ "verdict": "MISS",
461
+ "predicted": "UP",
462
+ "actual": "DOWN"
463
+ },
464
+ "n": {
465
+ "verdict": "MISS",
466
+ "predicted": "UP",
467
+ "actual": "DOWN"
468
+ }
469
+ },
470
+ "hits": 0,
471
+ "total_calls": 3,
472
+ "context": "January Monthly Expiration Day, Dow Up 13 of Last 15; Day Before MLK Day"
473
+ },
474
+ "2026-01-20": {
475
+ "actual": {
476
+ "dji": 48488.59,
477
+ "sp500": 6796.86,
478
+ "nasdaq": 22954.32
479
+ },
480
+ "prev_close": {
481
+ "dji": 49359.33,
482
+ "sp500": 6940.01,
483
+ "nasdaq": 23515.39
484
+ },
485
+ "pct_change": {
486
+ "dji": -0.017641,
487
+ "sp500": -0.020627,
488
+ "nasdaq": -0.02386
489
+ },
490
+ "almanac_scores": {
491
+ "d": 42.9,
492
+ "s": 42.9,
493
+ "n": 47.6
494
+ },
495
+ "results": {
496
+ "d": {
497
+ "verdict": "HIT",
498
+ "predicted": "DOWN",
499
+ "actual": "DOWN"
500
+ },
501
+ "s": {
502
+ "verdict": "HIT",
503
+ "predicted": "DOWN",
504
+ "actual": "DOWN"
505
+ },
506
+ "n": {
507
+ "verdict": "HIT",
508
+ "predicted": "DOWN",
509
+ "actual": "DOWN"
510
+ }
511
+ },
512
+ "hits": 3,
513
+ "total_calls": 3,
514
+ "context": "Day After Martin Luther King Jr. Day, NASDAQ Down 7 of Last 10"
515
+ },
516
+ "2026-01-21": {
517
+ "actual": {
518
+ "dji": 49077.23,
519
+ "sp500": 6875.62,
520
+ "nasdaq": 23224.82
521
+ },
522
+ "prev_close": {
523
+ "dji": 48488.59,
524
+ "sp500": 6796.86,
525
+ "nasdaq": 22954.32
526
+ },
527
+ "pct_change": {
528
+ "dji": 0.01214,
529
+ "sp500": 0.011588,
530
+ "nasdaq": 0.011784
531
+ },
532
+ "almanac_scores": {
533
+ "d": 52.4,
534
+ "s": 61.9,
535
+ "n": 47.6
536
+ },
537
+ "results": {
538
+ "d": {
539
+ "verdict": "HIT",
540
+ "predicted": "UP",
541
+ "actual": "UP"
542
+ },
543
+ "s": {
544
+ "verdict": "HIT",
545
+ "predicted": "UP",
546
+ "actual": "UP"
547
+ },
548
+ "n": {
549
+ "verdict": "MISS",
550
+ "predicted": "DOWN",
551
+ "actual": "UP"
552
+ }
553
+ },
554
+ "hits": 2,
555
+ "total_calls": 3,
556
+ "context": ""
557
+ },
558
+ "2026-01-22": {
559
+ "actual": {
560
+ "dji": 49384.01,
561
+ "sp500": 6913.35,
562
+ "nasdaq": 23436.02
563
+ },
564
+ "prev_close": {
565
+ "dji": 49077.23,
566
+ "sp500": 6875.62,
567
+ "nasdaq": 23224.82
568
+ },
569
+ "pct_change": {
570
+ "dji": 0.006251,
571
+ "sp500": 0.005488,
572
+ "nasdaq": 0.009094
573
+ },
574
+ "almanac_scores": {
575
+ "d": 42.9,
576
+ "s": 57.1,
577
+ "n": 52.4
578
+ },
579
+ "results": {
580
+ "d": {
581
+ "verdict": "MISS",
582
+ "predicted": "DOWN",
583
+ "actual": "UP"
584
+ },
585
+ "s": {
586
+ "verdict": "HIT",
587
+ "predicted": "UP",
588
+ "actual": "UP"
589
+ },
590
+ "n": {
591
+ "verdict": "HIT",
592
+ "predicted": "UP",
593
+ "actual": "UP"
594
+ }
595
+ },
596
+ "hits": 2,
597
+ "total_calls": 3,
598
+ "context": ""
599
+ },
600
+ "2026-01-23": {
601
+ "actual": {
602
+ "dji": 49098.71,
603
+ "sp500": 6915.61,
604
+ "nasdaq": 23501.24
605
+ },
606
+ "prev_close": {
607
+ "dji": 49384.01,
608
+ "sp500": 6913.35,
609
+ "nasdaq": 23436.02
610
+ },
611
+ "pct_change": {
612
+ "dji": -0.005777,
613
+ "sp500": 0.000327,
614
+ "nasdaq": 0.002783
615
+ },
616
+ "almanac_scores": {
617
+ "d": 47.6,
618
+ "s": 52.4,
619
+ "n": 66.7
620
+ },
621
+ "results": {
622
+ "d": {
623
+ "verdict": "HIT",
624
+ "predicted": "DOWN",
625
+ "actual": "DOWN"
626
+ },
627
+ "s": {
628
+ "verdict": "HIT",
629
+ "predicted": "UP",
630
+ "actual": "UP"
631
+ },
632
+ "n": {
633
+ "verdict": "HIT",
634
+ "predicted": "UP",
635
+ "actual": "UP"
636
+ }
637
+ },
638
+ "hits": 3,
639
+ "total_calls": 3,
640
+ "context": ""
641
+ },
642
+ "2026-01-26": {
643
+ "actual": {
644
+ "dji": 49412.4,
645
+ "sp500": 6950.23,
646
+ "nasdaq": 23601.36
647
+ },
648
+ "prev_close": {
649
+ "dji": 49098.71,
650
+ "sp500": 6915.61,
651
+ "nasdaq": 23501.24
652
+ },
653
+ "pct_change": {
654
+ "dji": 0.006389,
655
+ "sp500": 0.005006,
656
+ "nasdaq": 0.00426
657
+ },
658
+ "almanac_scores": {
659
+ "d": 57.1,
660
+ "s": 61.9,
661
+ "n": 57.1
662
+ },
663
+ "results": {
664
+ "d": {
665
+ "verdict": "HIT",
666
+ "predicted": "UP",
667
+ "actual": "UP"
668
+ },
669
+ "s": {
670
+ "verdict": "HIT",
671
+ "predicted": "UP",
672
+ "actual": "UP"
673
+ },
674
+ "n": {
675
+ "verdict": "HIT",
676
+ "predicted": "UP",
677
+ "actual": "UP"
678
+ }
679
+ },
680
+ "hits": 3,
681
+ "total_calls": 3,
682
+ "context": ""
683
+ },
684
+ "2026-01-27": {
685
+ "actual": {
686
+ "dji": 49003.41,
687
+ "sp500": 6978.6,
688
+ "nasdaq": 23817.1
689
+ },
690
+ "prev_close": {
691
+ "dji": 49412.4,
692
+ "sp500": 6950.23,
693
+ "nasdaq": 23601.36
694
+ },
695
+ "pct_change": {
696
+ "dji": -0.008277,
697
+ "sp500": 0.004082,
698
+ "nasdaq": 0.009141
699
+ },
700
+ "almanac_scores": {
701
+ "d": 57.1,
702
+ "s": 47.6,
703
+ "n": 61.9
704
+ },
705
+ "results": {
706
+ "d": {
707
+ "verdict": "MISS",
708
+ "predicted": "UP",
709
+ "actual": "DOWN"
710
+ },
711
+ "s": {
712
+ "verdict": "MISS",
713
+ "predicted": "DOWN",
714
+ "actual": "UP"
715
+ },
716
+ "n": {
717
+ "verdict": "HIT",
718
+ "predicted": "UP",
719
+ "actual": "UP"
720
+ }
721
+ },
722
+ "hits": 1,
723
+ "total_calls": 3,
724
+ "context": ""
725
+ },
726
+ "2026-01-28": {
727
+ "actual": {
728
+ "dji": 49015.6,
729
+ "sp500": 6978.03,
730
+ "nasdaq": 23857.45
731
+ },
732
+ "prev_close": {
733
+ "dji": 49003.41,
734
+ "sp500": 6978.6,
735
+ "nasdaq": 23817.1
736
+ },
737
+ "pct_change": {
738
+ "dji": 0.000249,
739
+ "sp500": -8.2e-05,
740
+ "nasdaq": 0.001694
741
+ },
742
+ "almanac_scores": {
743
+ "d": 52.4,
744
+ "s": 42.9,
745
+ "n": 57.1
746
+ },
747
+ "results": {
748
+ "d": {
749
+ "verdict": "HIT",
750
+ "predicted": "UP",
751
+ "actual": "UP"
752
+ },
753
+ "s": {
754
+ "verdict": "HIT",
755
+ "predicted": "DOWN",
756
+ "actual": "DOWN"
757
+ },
758
+ "n": {
759
+ "verdict": "HIT",
760
+ "predicted": "UP",
761
+ "actual": "UP"
762
+ }
763
+ },
764
+ "hits": 3,
765
+ "total_calls": 3,
766
+ "context": "FOMC Meeting"
767
+ },
768
+ "2026-01-29": {
769
+ "actual": {
770
+ "dji": 49071.56,
771
+ "sp500": 6969.01,
772
+ "nasdaq": 23685.12
773
+ },
774
+ "prev_close": {
775
+ "dji": 49015.6,
776
+ "sp500": 6978.03,
777
+ "nasdaq": 23857.45
778
+ },
779
+ "pct_change": {
780
+ "dji": 0.001142,
781
+ "sp500": -0.001293,
782
+ "nasdaq": -0.007223
783
+ },
784
+ "almanac_scores": {
785
+ "d": 47.6,
786
+ "s": 47.6,
787
+ "n": 42.9
788
+ },
789
+ "results": {
790
+ "d": {
791
+ "verdict": "MISS",
792
+ "predicted": "DOWN",
793
+ "actual": "UP"
794
+ },
795
+ "s": {
796
+ "verdict": "HIT",
797
+ "predicted": "DOWN",
798
+ "actual": "DOWN"
799
+ },
800
+ "n": {
801
+ "verdict": "HIT",
802
+ "predicted": "DOWN",
803
+ "actual": "DOWN"
804
+ }
805
+ },
806
+ "hits": 2,
807
+ "total_calls": 3,
808
+ "context": ""
809
+ },
810
+ "2026-01-30": {
811
+ "actual": {
812
+ "dji": 48892.47,
813
+ "sp500": 6939.03,
814
+ "nasdaq": 23461.82
815
+ },
816
+ "prev_close": {
817
+ "dji": 49071.56,
818
+ "sp500": 6969.01,
819
+ "nasdaq": 23685.12
820
+ },
821
+ "pct_change": {
822
+ "dji": -0.00365,
823
+ "sp500": -0.004302,
824
+ "nasdaq": -0.009428
825
+ },
826
+ "almanac_scores": {
827
+ "d": 38.1,
828
+ "s": 42.9,
829
+ "n": 52.4
830
+ },
831
+ "results": {
832
+ "d": {
833
+ "verdict": "HIT",
834
+ "predicted": "DOWN",
835
+ "actual": "DOWN"
836
+ },
837
+ "s": {
838
+ "verdict": "HIT",
839
+ "predicted": "DOWN",
840
+ "actual": "DOWN"
841
+ },
842
+ "n": {
843
+ "verdict": "MISS",
844
+ "predicted": "UP",
845
+ "actual": "DOWN"
846
+ }
847
+ },
848
+ "hits": 2,
849
+ "total_calls": 3,
850
+ "context": "January Barometer 84.0% Accurate"
851
+ },
852
+ "2026-02-02": {
853
+ "actual": {
854
+ "dji": 49407.66,
855
+ "sp500": 6976.44,
856
+ "nasdaq": 23592.11
857
+ },
858
+ "prev_close": {
859
+ "dji": 48892.47,
860
+ "sp500": 6939.03,
861
+ "nasdaq": 23461.82
862
+ },
863
+ "pct_change": {
864
+ "dji": 0.010537,
865
+ "sp500": 0.005391,
866
+ "nasdaq": 0.005553
867
+ },
868
+ "almanac_scores": {
869
+ "d": 85.7,
870
+ "s": 81.0,
871
+ "n": 81.0
872
+ },
873
+ "results": {
874
+ "d": {
875
+ "verdict": "HIT",
876
+ "predicted": "UP",
877
+ "actual": "UP"
878
+ },
879
+ "s": {
880
+ "verdict": "HIT",
881
+ "predicted": "UP",
882
+ "actual": "UP"
883
+ },
884
+ "n": {
885
+ "verdict": "HIT",
886
+ "predicted": "UP",
887
+ "actual": "UP"
888
+ }
889
+ },
890
+ "hits": 3,
891
+ "total_calls": 3,
892
+ "context": "First Trading Day in February, Dow Up 19 of Last 23"
893
+ },
894
+ "2026-02-03": {
895
+ "actual": {
896
+ "dji": 49240.99,
897
+ "sp500": 6917.81,
898
+ "nasdaq": 23255.19
899
+ },
900
+ "prev_close": {
901
+ "dji": 49407.66,
902
+ "sp500": 6976.44,
903
+ "nasdaq": 23592.11
904
+ },
905
+ "pct_change": {
906
+ "dji": -0.003373,
907
+ "sp500": -0.008404,
908
+ "nasdaq": -0.014281
909
+ },
910
+ "almanac_scores": {
911
+ "d": 57.1,
912
+ "s": 71.4,
913
+ "n": 66.7
914
+ },
915
+ "results": {
916
+ "d": {
917
+ "verdict": "MISS",
918
+ "predicted": "UP",
919
+ "actual": "DOWN"
920
+ },
921
+ "s": {
922
+ "verdict": "MISS",
923
+ "predicted": "UP",
924
+ "actual": "DOWN"
925
+ },
926
+ "n": {
927
+ "verdict": "MISS",
928
+ "predicted": "UP",
929
+ "actual": "DOWN"
930
+ }
931
+ },
932
+ "hits": 0,
933
+ "total_calls": 3,
934
+ "context": ""
935
+ },
936
+ "2026-02-04": {
937
+ "actual": {
938
+ "dji": 49501.3,
939
+ "sp500": 6882.72,
940
+ "nasdaq": 22904.58
941
+ },
942
+ "prev_close": {
943
+ "dji": 49240.99,
944
+ "sp500": 6917.81,
945
+ "nasdaq": 23255.19
946
+ },
947
+ "pct_change": {
948
+ "dji": 0.005286,
949
+ "sp500": -0.005072,
950
+ "nasdaq": -0.015077
951
+ },
952
+ "almanac_scores": {
953
+ "d": 47.6,
954
+ "s": 38.1,
955
+ "n": 33.3
956
+ },
957
+ "results": {
958
+ "d": {
959
+ "verdict": "MISS",
960
+ "predicted": "DOWN",
961
+ "actual": "UP"
962
+ },
963
+ "s": {
964
+ "verdict": "HIT",
965
+ "predicted": "DOWN",
966
+ "actual": "DOWN"
967
+ },
968
+ "n": {
969
+ "verdict": "HIT",
970
+ "predicted": "DOWN",
971
+ "actual": "DOWN"
972
+ }
973
+ },
974
+ "hits": 2,
975
+ "total_calls": 3,
976
+ "context": ""
977
+ },
978
+ "2026-02-05": {
979
+ "actual": {
980
+ "dji": 48908.72,
981
+ "sp500": 6798.4,
982
+ "nasdaq": 22540.59
983
+ },
984
+ "prev_close": {
985
+ "dji": 49501.3,
986
+ "sp500": 6882.72,
987
+ "nasdaq": 22904.58
988
+ },
989
+ "pct_change": {
990
+ "dji": -0.011971,
991
+ "sp500": -0.012251,
992
+ "nasdaq": -0.015892
993
+ },
994
+ "almanac_scores": {
995
+ "d": 66.7,
996
+ "s": 71.4,
997
+ "n": 61.9
998
+ },
999
+ "results": {
1000
+ "d": {
1001
+ "verdict": "MISS",
1002
+ "predicted": "UP",
1003
+ "actual": "DOWN"
1004
+ },
1005
+ "s": {
1006
+ "verdict": "MISS",
1007
+ "predicted": "UP",
1008
+ "actual": "DOWN"
1009
+ },
1010
+ "n": {
1011
+ "verdict": "MISS",
1012
+ "predicted": "UP",
1013
+ "actual": "DOWN"
1014
+ }
1015
+ },
1016
+ "hits": 0,
1017
+ "total_calls": 3,
1018
+ "context": ""
1019
+ },
1020
+ "2026-02-06": {
1021
+ "actual": {
1022
+ "dji": 50115.67,
1023
+ "sp500": 6932.3,
1024
+ "nasdaq": 23031.21
1025
+ },
1026
+ "prev_close": {
1027
+ "dji": 48908.72,
1028
+ "sp500": 6798.4,
1029
+ "nasdaq": 22540.59
1030
+ },
1031
+ "pct_change": {
1032
+ "dji": 0.024678,
1033
+ "sp500": 0.019696,
1034
+ "nasdaq": 0.021766
1035
+ },
1036
+ "almanac_scores": {
1037
+ "d": 61.9,
1038
+ "s": 57.1,
1039
+ "n": 57.1
1040
+ },
1041
+ "results": {
1042
+ "d": {
1043
+ "verdict": "HIT",
1044
+ "predicted": "UP",
1045
+ "actual": "UP"
1046
+ },
1047
+ "s": {
1048
+ "verdict": "HIT",
1049
+ "predicted": "UP",
1050
+ "actual": "UP"
1051
+ },
1052
+ "n": {
1053
+ "verdict": "HIT",
1054
+ "predicted": "UP",
1055
+ "actual": "UP"
1056
+ }
1057
+ },
1058
+ "hits": 3,
1059
+ "total_calls": 3,
1060
+ "context": ""
1061
+ },
1062
+ "2026-02-09": {
1063
+ "actual": {
1064
+ "dji": 50135.87,
1065
+ "sp500": 6964.82,
1066
+ "nasdaq": 23238.67
1067
+ },
1068
+ "prev_close": {
1069
+ "dji": 50115.67,
1070
+ "sp500": 6932.3,
1071
+ "nasdaq": 23031.21
1072
+ },
1073
+ "pct_change": {
1074
+ "dji": 0.000403,
1075
+ "sp500": 0.004691,
1076
+ "nasdaq": 0.009008
1077
+ },
1078
+ "almanac_scores": {
1079
+ "d": 47.6,
1080
+ "s": 61.9,
1081
+ "n": 61.9
1082
+ },
1083
+ "results": {
1084
+ "d": {
1085
+ "verdict": "MISS",
1086
+ "predicted": "DOWN",
1087
+ "actual": "UP"
1088
+ },
1089
+ "s": {
1090
+ "verdict": "HIT",
1091
+ "predicted": "UP",
1092
+ "actual": "UP"
1093
+ },
1094
+ "n": {
1095
+ "verdict": "HIT",
1096
+ "predicted": "UP",
1097
+ "actual": "UP"
1098
+ }
1099
+ },
1100
+ "hits": 2,
1101
+ "total_calls": 3,
1102
+ "context": ""
1103
+ },
1104
+ "2026-02-10": {
1105
+ "actual": {
1106
+ "dji": 50188.14,
1107
+ "sp500": 6941.81,
1108
+ "nasdaq": 23102.47
1109
+ },
1110
+ "prev_close": {
1111
+ "dji": 50135.87,
1112
+ "sp500": 6964.82,
1113
+ "nasdaq": 23238.67
1114
+ },
1115
+ "pct_change": {
1116
+ "dji": 0.001043,
1117
+ "sp500": -0.003304,
1118
+ "nasdaq": -0.005861
1119
+ },
1120
+ "almanac_scores": {
1121
+ "d": 52.4,
1122
+ "s": 57.1,
1123
+ "n": 61.9
1124
+ },
1125
+ "results": {
1126
+ "d": {
1127
+ "verdict": "HIT",
1128
+ "predicted": "UP",
1129
+ "actual": "UP"
1130
+ },
1131
+ "s": {
1132
+ "verdict": "MISS",
1133
+ "predicted": "UP",
1134
+ "actual": "DOWN"
1135
+ },
1136
+ "n": {
1137
+ "verdict": "MISS",
1138
+ "predicted": "UP",
1139
+ "actual": "DOWN"
1140
+ }
1141
+ },
1142
+ "hits": 1,
1143
+ "total_calls": 3,
1144
+ "context": "Week Before February Monthly Expiration Week, NASDAQ Up 11 of Last 16"
1145
+ },
1146
+ "2026-02-11": {
1147
+ "actual": {
1148
+ "dji": 50121.4,
1149
+ "sp500": 6941.47,
1150
+ "nasdaq": 23066.47
1151
+ },
1152
+ "prev_close": {
1153
+ "dji": 50188.14,
1154
+ "sp500": 6941.81,
1155
+ "nasdaq": 23102.47
1156
+ },
1157
+ "pct_change": {
1158
+ "dji": -0.00133,
1159
+ "sp500": -4.9e-05,
1160
+ "nasdaq": -0.001558
1161
+ },
1162
+ "almanac_scores": {
1163
+ "d": 61.9,
1164
+ "s": 57.1,
1165
+ "n": 57.1
1166
+ },
1167
+ "results": {
1168
+ "d": {
1169
+ "verdict": "MISS",
1170
+ "predicted": "UP",
1171
+ "actual": "DOWN"
1172
+ },
1173
+ "s": {
1174
+ "verdict": "MISS",
1175
+ "predicted": "UP",
1176
+ "actual": "DOWN"
1177
+ },
1178
+ "n": {
1179
+ "verdict": "MISS",
1180
+ "predicted": "UP",
1181
+ "actual": "DOWN"
1182
+ }
1183
+ },
1184
+ "hits": 0,
1185
+ "total_calls": 3,
1186
+ "context": ""
1187
+ },
1188
+ "2026-02-12": {
1189
+ "actual": {
1190
+ "dji": 49451.98,
1191
+ "sp500": 6832.76,
1192
+ "nasdaq": 22597.15
1193
+ },
1194
+ "prev_close": {
1195
+ "dji": 50121.4,
1196
+ "sp500": 6941.47,
1197
+ "nasdaq": 23066.47
1198
+ },
1199
+ "pct_change": {
1200
+ "dji": -0.013356,
1201
+ "sp500": -0.015661,
1202
+ "nasdaq": -0.020346
1203
+ },
1204
+ "almanac_scores": {
1205
+ "d": 57.1,
1206
+ "s": 71.4,
1207
+ "n": 71.4
1208
+ },
1209
+ "results": {
1210
+ "d": {
1211
+ "verdict": "MISS",
1212
+ "predicted": "UP",
1213
+ "actual": "DOWN"
1214
+ },
1215
+ "s": {
1216
+ "verdict": "MISS",
1217
+ "predicted": "UP",
1218
+ "actual": "DOWN"
1219
+ },
1220
+ "n": {
1221
+ "verdict": "MISS",
1222
+ "predicted": "UP",
1223
+ "actual": "DOWN"
1224
+ }
1225
+ },
1226
+ "hits": 0,
1227
+ "total_calls": 3,
1228
+ "context": ""
1229
+ },
1230
+ "2026-02-13": {
1231
+ "actual": {
1232
+ "dji": 49500.93,
1233
+ "sp500": 6836.17,
1234
+ "nasdaq": 22546.67
1235
+ },
1236
+ "prev_close": {
1237
+ "dji": 49451.98,
1238
+ "sp500": 6832.76,
1239
+ "nasdaq": 22597.15
1240
+ },
1241
+ "pct_change": {
1242
+ "dji": 0.00099,
1243
+ "sp500": 0.000499,
1244
+ "nasdaq": -0.002234
1245
+ },
1246
+ "almanac_scores": {
1247
+ "d": 47.6,
1248
+ "s": 61.9,
1249
+ "n": 81.0
1250
+ },
1251
+ "results": {
1252
+ "d": {
1253
+ "verdict": "MISS",
1254
+ "predicted": "DOWN",
1255
+ "actual": "UP"
1256
+ },
1257
+ "s": {
1258
+ "verdict": "HIT",
1259
+ "predicted": "UP",
1260
+ "actual": "UP"
1261
+ },
1262
+ "n": {
1263
+ "verdict": "MISS",
1264
+ "predicted": "UP",
1265
+ "actual": "DOWN"
1266
+ }
1267
+ },
1268
+ "hits": 1,
1269
+ "total_calls": 3,
1270
+ "context": "Day Before Presidents' Day Weekend, S&P Up 10 of Last 15"
1271
+ },
1272
+ "2026-02-17": {
1273
+ "actual": {
1274
+ "dji": 49533.19,
1275
+ "sp500": 6843.22,
1276
+ "nasdaq": 22578.38
1277
+ },
1278
+ "prev_close": {
1279
+ "dji": 49500.93,
1280
+ "sp500": 6836.17,
1281
+ "nasdaq": 22546.67
1282
+ },
1283
+ "pct_change": {
1284
+ "dji": 0.000652,
1285
+ "sp500": 0.001031,
1286
+ "nasdaq": 0.001406
1287
+ },
1288
+ "almanac_scores": {
1289
+ "d": 71.4,
1290
+ "s": 71.4,
1291
+ "n": 71.4
1292
+ },
1293
+ "results": {
1294
+ "d": {
1295
+ "verdict": "HIT",
1296
+ "predicted": "UP",
1297
+ "actual": "UP"
1298
+ },
1299
+ "s": {
1300
+ "verdict": "HIT",
1301
+ "predicted": "UP",
1302
+ "actual": "UP"
1303
+ },
1304
+ "n": {
1305
+ "verdict": "HIT",
1306
+ "predicted": "UP",
1307
+ "actual": "UP"
1308
+ }
1309
+ },
1310
+ "hits": 3,
1311
+ "total_calls": 3,
1312
+ "context": "Day After Presidents Day; First Trading Day of February Monthly Expiration Week, Dow Up 22 of Last 32"
1313
+ },
1314
+ "2026-02-18": {
1315
+ "actual": {
1316
+ "dji": 49662.66,
1317
+ "sp500": 6881.31,
1318
+ "nasdaq": 22753.63
1319
+ },
1320
+ "prev_close": {
1321
+ "dji": 49533.19,
1322
+ "sp500": 6843.22,
1323
+ "nasdaq": 22578.38
1324
+ },
1325
+ "pct_change": {
1326
+ "dji": 0.002614,
1327
+ "sp500": 0.005566,
1328
+ "nasdaq": 0.007762
1329
+ },
1330
+ "almanac_scores": {
1331
+ "d": 61.9,
1332
+ "s": 52.4,
1333
+ "n": 42.9
1334
+ },
1335
+ "results": {
1336
+ "d": {
1337
+ "verdict": "HIT",
1338
+ "predicted": "UP",
1339
+ "actual": "UP"
1340
+ },
1341
+ "s": {
1342
+ "verdict": "HIT",
1343
+ "predicted": "UP",
1344
+ "actual": "UP"
1345
+ },
1346
+ "n": {
1347
+ "verdict": "MISS",
1348
+ "predicted": "DOWN",
1349
+ "actual": "UP"
1350
+ }
1351
+ },
1352
+ "hits": 2,
1353
+ "total_calls": 3,
1354
+ "context": "Ash Wednesday"
1355
+ },
1356
+ "2026-02-19": {
1357
+ "actual": {
1358
+ "dji": 49395.16,
1359
+ "sp500": 6861.89,
1360
+ "nasdaq": 22682.73
1361
+ },
1362
+ "prev_close": {
1363
+ "dji": 49662.66,
1364
+ "sp500": 6881.31,
1365
+ "nasdaq": 22753.63
1366
+ },
1367
+ "pct_change": {
1368
+ "dji": -0.005386,
1369
+ "sp500": -0.002822,
1370
+ "nasdaq": -0.003116
1371
+ },
1372
+ "almanac_scores": {
1373
+ "d": 42.9,
1374
+ "s": 38.1,
1375
+ "n": 38.1
1376
+ },
1377
+ "results": {
1378
+ "d": {
1379
+ "verdict": "HIT",
1380
+ "predicted": "DOWN",
1381
+ "actual": "DOWN"
1382
+ },
1383
+ "s": {
1384
+ "verdict": "HIT",
1385
+ "predicted": "DOWN",
1386
+ "actual": "DOWN"
1387
+ },
1388
+ "n": {
1389
+ "verdict": "HIT",
1390
+ "predicted": "DOWN",
1391
+ "actual": "DOWN"
1392
+ }
1393
+ },
1394
+ "hits": 3,
1395
+ "total_calls": 3,
1396
+ "context": ""
1397
+ },
1398
+ "2026-02-20": {
1399
+ "actual": {
1400
+ "dji": 49625.97,
1401
+ "sp500": 6909.51,
1402
+ "nasdaq": 22886.07
1403
+ },
1404
+ "prev_close": {
1405
+ "dji": 49395.16,
1406
+ "sp500": 6861.89,
1407
+ "nasdaq": 22682.73
1408
+ },
1409
+ "pct_change": {
1410
+ "dji": 0.004673,
1411
+ "sp500": 0.00694,
1412
+ "nasdaq": 0.008965
1413
+ },
1414
+ "almanac_scores": {
1415
+ "d": 38.1,
1416
+ "s": 33.3,
1417
+ "n": 33.3
1418
+ },
1419
+ "results": {
1420
+ "d": {
1421
+ "verdict": "MISS",
1422
+ "predicted": "DOWN",
1423
+ "actual": "UP"
1424
+ },
1425
+ "s": {
1426
+ "verdict": "MISS",
1427
+ "predicted": "DOWN",
1428
+ "actual": "UP"
1429
+ },
1430
+ "n": {
1431
+ "verdict": "MISS",
1432
+ "predicted": "DOWN",
1433
+ "actual": "UP"
1434
+ }
1435
+ },
1436
+ "hits": 0,
1437
+ "total_calls": 3,
1438
+ "context": "February Monthly Expiration Day, NASDAQ Down 15 of Last 22"
1439
+ },
1440
+ "2026-02-23": {
1441
+ "actual": {
1442
+ "dji": 48804.06,
1443
+ "sp500": 6837.75,
1444
+ "nasdaq": 22627.27
1445
+ },
1446
+ "prev_close": {
1447
+ "dji": 49625.97,
1448
+ "sp500": 6909.51,
1449
+ "nasdaq": 22886.07
1450
+ },
1451
+ "pct_change": {
1452
+ "dji": -0.016562,
1453
+ "sp500": -0.010386,
1454
+ "nasdaq": -0.011308
1455
+ },
1456
+ "almanac_scores": {
1457
+ "d": 47.6,
1458
+ "s": 38.1,
1459
+ "n": 42.9
1460
+ },
1461
+ "results": {
1462
+ "d": {
1463
+ "verdict": "HIT",
1464
+ "predicted": "DOWN",
1465
+ "actual": "DOWN"
1466
+ },
1467
+ "s": {
1468
+ "verdict": "HIT",
1469
+ "predicted": "DOWN",
1470
+ "actual": "DOWN"
1471
+ },
1472
+ "n": {
1473
+ "verdict": "HIT",
1474
+ "predicted": "DOWN",
1475
+ "actual": "DOWN"
1476
+ }
1477
+ },
1478
+ "hits": 3,
1479
+ "total_calls": 3,
1480
+ "context": "End of February Miserable in Recent Years"
1481
+ },
1482
+ "2026-02-24": {
1483
+ "actual": {
1484
+ "dji": 49174.5,
1485
+ "sp500": 6890.07,
1486
+ "nasdaq": 22863.68
1487
+ },
1488
+ "prev_close": {
1489
+ "dji": 48804.06,
1490
+ "sp500": 6837.75,
1491
+ "nasdaq": 22627.27
1492
+ },
1493
+ "pct_change": {
1494
+ "dji": 0.00759,
1495
+ "sp500": 0.007652,
1496
+ "nasdaq": 0.010448
1497
+ },
1498
+ "almanac_scores": {
1499
+ "d": 47.6,
1500
+ "s": 52.4,
1501
+ "n": 42.9
1502
+ },
1503
+ "results": {
1504
+ "d": {
1505
+ "verdict": "MISS",
1506
+ "predicted": "DOWN",
1507
+ "actual": "UP"
1508
+ },
1509
+ "s": {
1510
+ "verdict": "HIT",
1511
+ "predicted": "UP",
1512
+ "actual": "UP"
1513
+ },
1514
+ "n": {
1515
+ "verdict": "MISS",
1516
+ "predicted": "DOWN",
1517
+ "actual": "UP"
1518
+ }
1519
+ },
1520
+ "hits": 1,
1521
+ "total_calls": 3,
1522
+ "context": "Week After February Monthly Expiration Week, Dow Down 15 of Last 27"
1523
+ },
1524
+ "2026-02-25": {
1525
+ "actual": {
1526
+ "dji": 49482.15,
1527
+ "sp500": 6946.13,
1528
+ "nasdaq": 23152.08
1529
+ },
1530
+ "prev_close": {
1531
+ "dji": 49174.5,
1532
+ "sp500": 6890.07,
1533
+ "nasdaq": 22863.68
1534
+ },
1535
+ "pct_change": {
1536
+ "dji": 0.006256,
1537
+ "sp500": 0.008136,
1538
+ "nasdaq": 0.012614
1539
+ },
1540
+ "almanac_scores": {
1541
+ "d": 57.1,
1542
+ "s": 61.9,
1543
+ "n": 76.2
1544
+ },
1545
+ "results": {
1546
+ "d": {
1547
+ "verdict": "HIT",
1548
+ "predicted": "UP",
1549
+ "actual": "UP"
1550
+ },
1551
+ "s": {
1552
+ "verdict": "HIT",
1553
+ "predicted": "UP",
1554
+ "actual": "UP"
1555
+ },
1556
+ "n": {
1557
+ "verdict": "HIT",
1558
+ "predicted": "UP",
1559
+ "actual": "UP"
1560
+ }
1561
+ },
1562
+ "hits": 3,
1563
+ "total_calls": 3,
1564
+ "context": ""
1565
+ },
1566
+ "2026-02-26": {
1567
+ "actual": {
1568
+ "dji": 49499.2,
1569
+ "sp500": 6908.86,
1570
+ "nasdaq": 22878.38
1571
+ },
1572
+ "prev_close": {
1573
+ "dji": 49482.15,
1574
+ "sp500": 6946.13,
1575
+ "nasdaq": 23152.08
1576
+ },
1577
+ "pct_change": {
1578
+ "dji": 0.000345,
1579
+ "sp500": -0.005366,
1580
+ "nasdaq": -0.011822
1581
+ },
1582
+ "almanac_scores": {
1583
+ "d": 42.9,
1584
+ "s": 47.6,
1585
+ "n": 61.9
1586
+ },
1587
+ "results": {
1588
+ "d": {
1589
+ "verdict": "MISS",
1590
+ "predicted": "DOWN",
1591
+ "actual": "UP"
1592
+ },
1593
+ "s": {
1594
+ "verdict": "HIT",
1595
+ "predicted": "DOWN",
1596
+ "actual": "DOWN"
1597
+ },
1598
+ "n": {
1599
+ "verdict": "MISS",
1600
+ "predicted": "UP",
1601
+ "actual": "DOWN"
1602
+ }
1603
+ },
1604
+ "hits": 1,
1605
+ "total_calls": 3,
1606
+ "context": ""
1607
+ },
1608
+ "2026-02-27": {
1609
+ "actual": {
1610
+ "dji": 48977.92,
1611
+ "sp500": 6878.88,
1612
+ "nasdaq": 22668.21
1613
+ },
1614
+ "prev_close": {
1615
+ "dji": 49499.2,
1616
+ "sp500": 6908.86,
1617
+ "nasdaq": 22878.38
1618
+ },
1619
+ "pct_change": {
1620
+ "dji": -0.010531,
1621
+ "sp500": -0.004339,
1622
+ "nasdaq": -0.009186
1623
+ },
1624
+ "almanac_scores": {
1625
+ "d": 28.6,
1626
+ "s": 28.6,
1627
+ "n": 33.3
1628
+ },
1629
+ "results": {
1630
+ "d": {
1631
+ "verdict": "HIT",
1632
+ "predicted": "DOWN",
1633
+ "actual": "DOWN"
1634
+ },
1635
+ "s": {
1636
+ "verdict": "HIT",
1637
+ "predicted": "DOWN",
1638
+ "actual": "DOWN"
1639
+ },
1640
+ "n": {
1641
+ "verdict": "HIT",
1642
+ "predicted": "DOWN",
1643
+ "actual": "DOWN"
1644
+ }
1645
+ },
1646
+ "hits": 3,
1647
+ "total_calls": 3,
1648
+ "context": ""
1649
+ },
1650
+ "2026-03-02": {
1651
+ "actual": {
1652
+ "dji": 48904.78,
1653
+ "sp500": 6881.62,
1654
+ "nasdaq": 22748.86
1655
+ },
1656
+ "prev_close": {
1657
+ "dji": 48977.92,
1658
+ "sp500": 6878.88,
1659
+ "nasdaq": 22668.21
1660
+ },
1661
+ "pct_change": {
1662
+ "dji": -0.001493,
1663
+ "sp500": 0.000398,
1664
+ "nasdaq": 0.003558
1665
+ },
1666
+ "almanac_scores": {
1667
+ "d": 66.7,
1668
+ "s": 66.7,
1669
+ "n": 61.9
1670
+ },
1671
+ "results": {
1672
+ "d": {
1673
+ "verdict": "MISS",
1674
+ "predicted": "UP",
1675
+ "actual": "DOWN"
1676
+ },
1677
+ "s": {
1678
+ "verdict": "HIT",
1679
+ "predicted": "UP",
1680
+ "actual": "UP"
1681
+ },
1682
+ "n": {
1683
+ "verdict": "HIT",
1684
+ "predicted": "UP",
1685
+ "actual": "UP"
1686
+ }
1687
+ },
1688
+ "hits": 2,
1689
+ "total_calls": 3,
1690
+ "context": "First Trading Day in March, S&P Up 17 of Last 26"
1691
+ },
1692
+ "2026-03-03": {
1693
+ "actual": {
1694
+ "dji": 48501.27,
1695
+ "sp500": 6816.63,
1696
+ "nasdaq": 22516.69
1697
+ },
1698
+ "prev_close": {
1699
+ "dji": 48904.78,
1700
+ "sp500": 6881.62,
1701
+ "nasdaq": 22748.86
1702
+ },
1703
+ "pct_change": {
1704
+ "dji": -0.008251,
1705
+ "sp500": -0.009444,
1706
+ "nasdaq": -0.010206
1707
+ },
1708
+ "almanac_scores": {
1709
+ "d": 33.3,
1710
+ "s": 38.1,
1711
+ "n": 42.9
1712
+ },
1713
+ "results": {
1714
+ "d": {
1715
+ "verdict": "HIT",
1716
+ "predicted": "DOWN",
1717
+ "actual": "DOWN"
1718
+ },
1719
+ "s": {
1720
+ "verdict": "HIT",
1721
+ "predicted": "DOWN",
1722
+ "actual": "DOWN"
1723
+ },
1724
+ "n": {
1725
+ "verdict": "HIT",
1726
+ "predicted": "DOWN",
1727
+ "actual": "DOWN"
1728
+ }
1729
+ },
1730
+ "hits": 3,
1731
+ "total_calls": 3,
1732
+ "context": ""
1733
+ },
1734
+ "2026-03-04": {
1735
+ "actual": {
1736
+ "dji": 48739.41,
1737
+ "sp500": 6869.5,
1738
+ "nasdaq": 22807.48
1739
+ },
1740
+ "prev_close": {
1741
+ "dji": 48501.27,
1742
+ "sp500": 6816.63,
1743
+ "nasdaq": 22516.69
1744
+ },
1745
+ "pct_change": {
1746
+ "dji": 0.00491,
1747
+ "sp500": 0.007756,
1748
+ "nasdaq": 0.012914
1749
+ },
1750
+ "almanac_scores": {
1751
+ "d": 52.4,
1752
+ "s": 57.1,
1753
+ "n": 47.6
1754
+ },
1755
+ "results": {
1756
+ "d": {
1757
+ "verdict": "HIT",
1758
+ "predicted": "UP",
1759
+ "actual": "UP"
1760
+ },
1761
+ "s": {
1762
+ "verdict": "HIT",
1763
+ "predicted": "UP",
1764
+ "actual": "UP"
1765
+ },
1766
+ "n": {
1767
+ "verdict": "MISS",
1768
+ "predicted": "DOWN",
1769
+ "actual": "UP"
1770
+ }
1771
+ },
1772
+ "hits": 2,
1773
+ "total_calls": 3,
1774
+ "context": ""
1775
+ },
1776
+ "2026-03-05": {
1777
+ "actual": {
1778
+ "dji": 47954.74,
1779
+ "sp500": 6830.71,
1780
+ "nasdaq": 22748.99
1781
+ },
1782
+ "prev_close": {
1783
+ "dji": 48739.41,
1784
+ "sp500": 6869.5,
1785
+ "nasdaq": 22807.48
1786
+ },
1787
+ "pct_change": {
1788
+ "dji": -0.016099,
1789
+ "sp500": -0.005647,
1790
+ "nasdaq": -0.002565
1791
+ },
1792
+ "almanac_scores": {
1793
+ "d": 47.6,
1794
+ "s": 52.4,
1795
+ "n": 38.1
1796
+ },
1797
+ "results": {
1798
+ "d": {
1799
+ "verdict": "HIT",
1800
+ "predicted": "DOWN",
1801
+ "actual": "DOWN"
1802
+ },
1803
+ "s": {
1804
+ "verdict": "MISS",
1805
+ "predicted": "UP",
1806
+ "actual": "DOWN"
1807
+ },
1808
+ "n": {
1809
+ "verdict": "HIT",
1810
+ "predicted": "DOWN",
1811
+ "actual": "DOWN"
1812
+ }
1813
+ },
1814
+ "hits": 2,
1815
+ "total_calls": 3,
1816
+ "context": "March Historically Strong Early in the Month"
1817
+ },
1818
+ "2026-03-06": {
1819
+ "actual": {
1820
+ "dji": 47501.55,
1821
+ "sp500": 6740.02,
1822
+ "nasdaq": 22387.68
1823
+ },
1824
+ "prev_close": {
1825
+ "dji": 47954.74,
1826
+ "sp500": 6830.71,
1827
+ "nasdaq": 22748.99
1828
+ },
1829
+ "pct_change": {
1830
+ "dji": -0.00945,
1831
+ "sp500": -0.013277,
1832
+ "nasdaq": -0.015882
1833
+ },
1834
+ "almanac_scores": {
1835
+ "d": 47.6,
1836
+ "s": 47.6,
1837
+ "n": 33.3
1838
+ },
1839
+ "results": {
1840
+ "d": {
1841
+ "verdict": "HIT",
1842
+ "predicted": "DOWN",
1843
+ "actual": "DOWN"
1844
+ },
1845
+ "s": {
1846
+ "verdict": "HIT",
1847
+ "predicted": "DOWN",
1848
+ "actual": "DOWN"
1849
+ },
1850
+ "n": {
1851
+ "verdict": "HIT",
1852
+ "predicted": "DOWN",
1853
+ "actual": "DOWN"
1854
+ }
1855
+ },
1856
+ "hits": 3,
1857
+ "total_calls": 3,
1858
+ "context": ""
1859
+ },
1860
+ "2026-03-09": {
1861
+ "actual": {
1862
+ "dji": 47740.8,
1863
+ "sp500": 6795.99,
1864
+ "nasdaq": 22695.95
1865
+ },
1866
+ "prev_close": {
1867
+ "dji": 47501.55,
1868
+ "sp500": 6740.02,
1869
+ "nasdaq": 22387.68
1870
+ },
1871
+ "pct_change": {
1872
+ "dji": 0.005037,
1873
+ "sp500": 0.008304,
1874
+ "nasdaq": 0.01377
1875
+ },
1876
+ "almanac_scores": {
1877
+ "d": 38.1,
1878
+ "s": 38.1,
1879
+ "n": 42.9
1880
+ },
1881
+ "results": {
1882
+ "d": {
1883
+ "verdict": "MISS",
1884
+ "predicted": "DOWN",
1885
+ "actual": "UP"
1886
+ },
1887
+ "s": {
1888
+ "verdict": "MISS",
1889
+ "predicted": "DOWN",
1890
+ "actual": "UP"
1891
+ },
1892
+ "n": {
1893
+ "verdict": "MISS",
1894
+ "predicted": "DOWN",
1895
+ "actual": "UP"
1896
+ }
1897
+ },
1898
+ "hits": 0,
1899
+ "total_calls": 3,
1900
+ "context": ""
1901
+ },
1902
+ "2026-03-10": {
1903
+ "actual": {
1904
+ "dji": 47706.51,
1905
+ "sp500": 6781.48,
1906
+ "nasdaq": 22697.1
1907
+ },
1908
+ "prev_close": {
1909
+ "dji": 47740.8,
1910
+ "sp500": 6795.99,
1911
+ "nasdaq": 22695.95
1912
+ },
1913
+ "pct_change": {
1914
+ "dji": -0.000718,
1915
+ "sp500": -0.002135,
1916
+ "nasdaq": 5.1e-05
1917
+ },
1918
+ "almanac_scores": {
1919
+ "d": 66.7,
1920
+ "s": 61.9,
1921
+ "n": 57.1
1922
+ },
1923
+ "results": {
1924
+ "d": {
1925
+ "verdict": "MISS",
1926
+ "predicted": "UP",
1927
+ "actual": "DOWN"
1928
+ },
1929
+ "s": {
1930
+ "verdict": "MISS",
1931
+ "predicted": "UP",
1932
+ "actual": "DOWN"
1933
+ },
1934
+ "n": {
1935
+ "verdict": "HIT",
1936
+ "predicted": "UP",
1937
+ "actual": "UP"
1938
+ }
1939
+ },
1940
+ "hits": 1,
1941
+ "total_calls": 3,
1942
+ "context": ""
1943
+ },
1944
+ "2026-03-11": {
1945
+ "actual": {
1946
+ "dji": 47417.27,
1947
+ "sp500": 6775.8,
1948
+ "nasdaq": 22716.13
1949
+ },
1950
+ "prev_close": {
1951
+ "dji": 47706.51,
1952
+ "sp500": 6781.48,
1953
+ "nasdaq": 22697.1
1954
+ },
1955
+ "pct_change": {
1956
+ "dji": -0.006063,
1957
+ "sp500": -0.000838,
1958
+ "nasdaq": 0.000838
1959
+ },
1960
+ "almanac_scores": {
1961
+ "d": 47.6,
1962
+ "s": 57.1,
1963
+ "n": 42.9
1964
+ },
1965
+ "results": {
1966
+ "d": {
1967
+ "verdict": "HIT",
1968
+ "predicted": "DOWN",
1969
+ "actual": "DOWN"
1970
+ },
1971
+ "s": {
1972
+ "verdict": "MISS",
1973
+ "predicted": "UP",
1974
+ "actual": "DOWN"
1975
+ },
1976
+ "n": {
1977
+ "verdict": "MISS",
1978
+ "predicted": "DOWN",
1979
+ "actual": "UP"
1980
+ }
1981
+ },
1982
+ "hits": 1,
1983
+ "total_calls": 3,
1984
+ "context": "Dow Down 1469 Points March 9-22 in 2001"
1985
+ },
1986
+ "2026-03-12": {
1987
+ "actual": {
1988
+ "dji": 46677.85,
1989
+ "sp500": 6672.62,
1990
+ "nasdaq": 22311.98
1991
+ },
1992
+ "prev_close": {
1993
+ "dji": 47417.27,
1994
+ "sp500": 6775.8,
1995
+ "nasdaq": 22716.13
1996
+ },
1997
+ "pct_change": {
1998
+ "dji": -0.015594,
1999
+ "sp500": -0.015228,
2000
+ "nasdaq": -0.017791
2001
+ },
2002
+ "almanac_scores": {
2003
+ "d": 52.4,
2004
+ "s": 57.1,
2005
+ "n": 61.9
2006
+ },
2007
+ "results": {
2008
+ "d": {
2009
+ "verdict": "MISS",
2010
+ "predicted": "UP",
2011
+ "actual": "DOWN"
2012
+ },
2013
+ "s": {
2014
+ "verdict": "MISS",
2015
+ "predicted": "UP",
2016
+ "actual": "DOWN"
2017
+ },
2018
+ "n": {
2019
+ "verdict": "MISS",
2020
+ "predicted": "UP",
2021
+ "actual": "DOWN"
2022
+ }
2023
+ },
2024
+ "hits": 0,
2025
+ "total_calls": 3,
2026
+ "context": ""
2027
+ },
2028
+ "2026-03-13": {
2029
+ "actual": {
2030
+ "dji": 46558.47,
2031
+ "sp500": 6632.19,
2032
+ "nasdaq": 22105.36
2033
+ },
2034
+ "prev_close": {
2035
+ "dji": 46677.85,
2036
+ "sp500": 6672.62,
2037
+ "nasdaq": 22311.98
2038
+ },
2039
+ "pct_change": {
2040
+ "dji": -0.002558,
2041
+ "sp500": -0.006059,
2042
+ "nasdaq": -0.00926
2043
+ },
2044
+ "almanac_scores": {
2045
+ "d": 66.7,
2046
+ "s": 42.9,
2047
+ "n": 47.6
2048
+ },
2049
+ "results": {
2050
+ "d": {
2051
+ "verdict": "MISS",
2052
+ "predicted": "UP",
2053
+ "actual": "DOWN"
2054
+ },
2055
+ "s": {
2056
+ "verdict": "HIT",
2057
+ "predicted": "DOWN",
2058
+ "actual": "DOWN"
2059
+ },
2060
+ "n": {
2061
+ "verdict": "HIT",
2062
+ "predicted": "DOWN",
2063
+ "actual": "DOWN"
2064
+ }
2065
+ },
2066
+ "hits": 2,
2067
+ "total_calls": 3,
2068
+ "context": ""
2069
+ },
2070
+ "2026-03-16": {
2071
+ "actual": {
2072
+ "dji": 46946.41,
2073
+ "sp500": 6699.38,
2074
+ "nasdaq": 22374.18
2075
+ },
2076
+ "prev_close": {
2077
+ "dji": 46558.47,
2078
+ "sp500": 6632.19,
2079
+ "nasdaq": 22105.36
2080
+ },
2081
+ "pct_change": {
2082
+ "dji": 0.008332,
2083
+ "sp500": 0.010131,
2084
+ "nasdaq": 0.012161
2085
+ },
2086
+ "almanac_scores": {
2087
+ "d": 61.9,
2088
+ "s": 47.6,
2089
+ "n": 47.6
2090
+ },
2091
+ "results": {
2092
+ "d": {
2093
+ "verdict": "HIT",
2094
+ "predicted": "UP",
2095
+ "actual": "UP"
2096
+ },
2097
+ "s": {
2098
+ "verdict": "MISS",
2099
+ "predicted": "DOWN",
2100
+ "actual": "UP"
2101
+ },
2102
+ "n": {
2103
+ "verdict": "MISS",
2104
+ "predicted": "DOWN",
2105
+ "actual": "UP"
2106
+ }
2107
+ },
2108
+ "hits": 1,
2109
+ "total_calls": 3,
2110
+ "context": "Monday Before March Triple Witching, Dow Up 27 of Last 38"
2111
+ },
2112
+ "2026-03-17": {
2113
+ "actual": {
2114
+ "dji": 46993.26,
2115
+ "sp500": 6716.09,
2116
+ "nasdaq": 22479.53
2117
+ },
2118
+ "prev_close": {
2119
+ "dji": 46946.41,
2120
+ "sp500": 6699.38,
2121
+ "nasdaq": 22374.18
2122
+ },
2123
+ "pct_change": {
2124
+ "dji": 0.000998,
2125
+ "sp500": 0.002494,
2126
+ "nasdaq": 0.004709
2127
+ },
2128
+ "almanac_scores": {
2129
+ "d": 61.9,
2130
+ "s": 66.7,
2131
+ "n": 71.4
2132
+ },
2133
+ "results": {
2134
+ "d": {
2135
+ "verdict": "HIT",
2136
+ "predicted": "UP",
2137
+ "actual": "UP"
2138
+ },
2139
+ "s": {
2140
+ "verdict": "HIT",
2141
+ "predicted": "UP",
2142
+ "actual": "UP"
2143
+ },
2144
+ "n": {
2145
+ "verdict": "HIT",
2146
+ "predicted": "UP",
2147
+ "actual": "UP"
2148
+ }
2149
+ },
2150
+ "hits": 3,
2151
+ "total_calls": 3,
2152
+ "context": "St. Patrick's Day"
2153
+ },
2154
+ "2026-03-18": {
2155
+ "actual": {
2156
+ "dji": 46225.15,
2157
+ "sp500": 6624.7,
2158
+ "nasdaq": 22152.42
2159
+ },
2160
+ "prev_close": {
2161
+ "dji": 46993.26,
2162
+ "sp500": 6716.09,
2163
+ "nasdaq": 22479.53
2164
+ },
2165
+ "pct_change": {
2166
+ "dji": -0.016345,
2167
+ "sp500": -0.013608,
2168
+ "nasdaq": -0.014551
2169
+ },
2170
+ "almanac_scores": {
2171
+ "d": 61.9,
2172
+ "s": 61.9,
2173
+ "n": 71.4
2174
+ },
2175
+ "results": {
2176
+ "d": {
2177
+ "verdict": "MISS",
2178
+ "predicted": "UP",
2179
+ "actual": "DOWN"
2180
+ },
2181
+ "s": {
2182
+ "verdict": "MISS",
2183
+ "predicted": "UP",
2184
+ "actual": "DOWN"
2185
+ },
2186
+ "n": {
2187
+ "verdict": "MISS",
2188
+ "predicted": "UP",
2189
+ "actual": "DOWN"
2190
+ }
2191
+ },
2192
+ "hits": 0,
2193
+ "total_calls": 3,
2194
+ "context": "FOMC Meeting"
2195
+ },
2196
+ "2026-03-19": {
2197
+ "actual": {
2198
+ "dji": 46021.43,
2199
+ "sp500": 6606.49,
2200
+ "nasdaq": 22090.69
2201
+ },
2202
+ "prev_close": {
2203
+ "dji": 46225.15,
2204
+ "sp500": 6624.7,
2205
+ "nasdaq": 22152.42
2206
+ },
2207
+ "pct_change": {
2208
+ "dji": -0.004407,
2209
+ "sp500": -0.002749,
2210
+ "nasdaq": -0.002787
2211
+ },
2212
+ "almanac_scores": {
2213
+ "d": 61.9,
2214
+ "s": 52.4,
2215
+ "n": 76.2
2216
+ },
2217
+ "results": {
2218
+ "d": {
2219
+ "verdict": "MISS",
2220
+ "predicted": "UP",
2221
+ "actual": "DOWN"
2222
+ },
2223
+ "s": {
2224
+ "verdict": "MISS",
2225
+ "predicted": "UP",
2226
+ "actual": "DOWN"
2227
+ },
2228
+ "n": {
2229
+ "verdict": "MISS",
2230
+ "predicted": "UP",
2231
+ "actual": "DOWN"
2232
+ }
2233
+ },
2234
+ "hits": 0,
2235
+ "total_calls": 3,
2236
+ "context": ""
2237
+ },
2238
+ "2026-03-20": {
2239
+ "actual": {
2240
+ "dji": 45577.47,
2241
+ "sp500": 6506.48,
2242
+ "nasdaq": 21647.61
2243
+ },
2244
+ "prev_close": {
2245
+ "dji": 46021.43,
2246
+ "sp500": 6606.49,
2247
+ "nasdaq": 22090.69
2248
+ },
2249
+ "pct_change": {
2250
+ "dji": -0.009647,
2251
+ "sp500": -0.015138,
2252
+ "nasdaq": -0.020057
2253
+ },
2254
+ "almanac_scores": {
2255
+ "d": 52.4,
2256
+ "s": 52.4,
2257
+ "n": 61.9
2258
+ },
2259
+ "results": {
2260
+ "d": {
2261
+ "verdict": "MISS",
2262
+ "predicted": "UP",
2263
+ "actual": "DOWN"
2264
+ },
2265
+ "s": {
2266
+ "verdict": "MISS",
2267
+ "predicted": "UP",
2268
+ "actual": "DOWN"
2269
+ },
2270
+ "n": {
2271
+ "verdict": "MISS",
2272
+ "predicted": "UP",
2273
+ "actual": "DOWN"
2274
+ }
2275
+ },
2276
+ "hits": 0,
2277
+ "total_calls": 3,
2278
+ "context": "March Triple-Witching Day Mixed Last 30 Years, NASDAQ Up 8 of Last 11"
2279
+ },
2280
+ "2026-03-23": {
2281
+ "actual": {
2282
+ "dji": 46208.47,
2283
+ "sp500": 6581.0,
2284
+ "nasdaq": 21946.76
2285
+ },
2286
+ "prev_close": {
2287
+ "dji": 45577.47,
2288
+ "sp500": 6506.48,
2289
+ "nasdaq": 21647.61
2290
+ },
2291
+ "pct_change": {
2292
+ "dji": 0.013845,
2293
+ "sp500": 0.011453,
2294
+ "nasdaq": 0.013819
2295
+ },
2296
+ "almanac_scores": {
2297
+ "d": 42.9,
2298
+ "s": 38.1,
2299
+ "n": 42.9
2300
+ },
2301
+ "results": {
2302
+ "d": {
2303
+ "verdict": "MISS",
2304
+ "predicted": "DOWN",
2305
+ "actual": "UP"
2306
+ },
2307
+ "s": {
2308
+ "verdict": "MISS",
2309
+ "predicted": "DOWN",
2310
+ "actual": "UP"
2311
+ },
2312
+ "n": {
2313
+ "verdict": "MISS",
2314
+ "predicted": "DOWN",
2315
+ "actual": "UP"
2316
+ }
2317
+ },
2318
+ "hits": 0,
2319
+ "total_calls": 3,
2320
+ "context": "Week after Triple Witching, Dow Down 23 of Last 38"
2321
+ },
2322
+ "2026-03-24": {
2323
+ "actual": {
2324
+ "dji": 46124.06,
2325
+ "sp500": 6556.37,
2326
+ "nasdaq": 21761.89
2327
+ },
2328
+ "prev_close": {
2329
+ "dji": 46208.47,
2330
+ "sp500": 6581.0,
2331
+ "nasdaq": 21946.76
2332
+ },
2333
+ "pct_change": {
2334
+ "dji": -0.001827,
2335
+ "sp500": -0.003743,
2336
+ "nasdaq": -0.008424
2337
+ },
2338
+ "almanac_scores": {
2339
+ "d": 42.9,
2340
+ "s": 47.6,
2341
+ "n": 52.4
2342
+ },
2343
+ "results": {
2344
+ "d": {
2345
+ "verdict": "HIT",
2346
+ "predicted": "DOWN",
2347
+ "actual": "DOWN"
2348
+ },
2349
+ "s": {
2350
+ "verdict": "HIT",
2351
+ "predicted": "DOWN",
2352
+ "actual": "DOWN"
2353
+ },
2354
+ "n": {
2355
+ "verdict": "MISS",
2356
+ "predicted": "UP",
2357
+ "actual": "DOWN"
2358
+ }
2359
+ },
2360
+ "hits": 2,
2361
+ "total_calls": 3,
2362
+ "context": "March Historically Weak Later in the Month"
2363
+ },
2364
+ "2026-03-25": {
2365
+ "actual": {
2366
+ "dji": 46429.49,
2367
+ "sp500": 6591.9,
2368
+ "nasdaq": 21929.83
2369
+ },
2370
+ "prev_close": {
2371
+ "dji": 46124.06,
2372
+ "sp500": 6556.37,
2373
+ "nasdaq": 21761.89
2374
+ },
2375
+ "pct_change": {
2376
+ "dji": 0.006622,
2377
+ "sp500": 0.005419,
2378
+ "nasdaq": 0.007717
2379
+ },
2380
+ "almanac_scores": {
2381
+ "d": 61.9,
2382
+ "s": 57.1,
2383
+ "n": 66.7
2384
+ },
2385
+ "results": {
2386
+ "d": {
2387
+ "verdict": "HIT",
2388
+ "predicted": "UP",
2389
+ "actual": "UP"
2390
+ },
2391
+ "s": {
2392
+ "verdict": "HIT",
2393
+ "predicted": "UP",
2394
+ "actual": "UP"
2395
+ },
2396
+ "n": {
2397
+ "verdict": "HIT",
2398
+ "predicted": "UP",
2399
+ "actual": "UP"
2400
+ }
2401
+ },
2402
+ "hits": 3,
2403
+ "total_calls": 3,
2404
+ "context": ""
2405
+ },
2406
+ "2026-03-26": {
2407
+ "actual": {
2408
+ "dji": 45960.11,
2409
+ "sp500": 6477.16,
2410
+ "nasdaq": 21408.08
2411
+ },
2412
+ "prev_close": {
2413
+ "dji": 46429.49,
2414
+ "sp500": 6591.9,
2415
+ "nasdaq": 21929.83
2416
+ },
2417
+ "pct_change": {
2418
+ "dji": -0.01011,
2419
+ "sp500": -0.017406,
2420
+ "nasdaq": -0.023792
2421
+ },
2422
+ "almanac_scores": {
2423
+ "d": 47.6,
2424
+ "s": 47.6,
2425
+ "n": 38.1
2426
+ },
2427
+ "results": {
2428
+ "d": {
2429
+ "verdict": "HIT",
2430
+ "predicted": "DOWN",
2431
+ "actual": "DOWN"
2432
+ },
2433
+ "s": {
2434
+ "verdict": "HIT",
2435
+ "predicted": "DOWN",
2436
+ "actual": "DOWN"
2437
+ },
2438
+ "n": {
2439
+ "verdict": "HIT",
2440
+ "predicted": "DOWN",
2441
+ "actual": "DOWN"
2442
+ }
2443
+ },
2444
+ "hits": 3,
2445
+ "total_calls": 3,
2446
+ "context": ""
2447
+ },
2448
+ "2026-03-27": {
2449
+ "actual": {
2450
+ "dji": 45166.64,
2451
+ "sp500": 6368.85,
2452
+ "nasdaq": 20948.36
2453
+ },
2454
+ "prev_close": {
2455
+ "dji": 45960.11,
2456
+ "sp500": 6477.16,
2457
+ "nasdaq": 21408.08
2458
+ },
2459
+ "pct_change": {
2460
+ "dji": -0.017264,
2461
+ "sp500": -0.016722,
2462
+ "nasdaq": -0.021474
2463
+ },
2464
+ "almanac_scores": {
2465
+ "d": 47.6,
2466
+ "s": 47.6,
2467
+ "n": 47.6
2468
+ },
2469
+ "results": {
2470
+ "d": {
2471
+ "verdict": "HIT",
2472
+ "predicted": "DOWN",
2473
+ "actual": "DOWN"
2474
+ },
2475
+ "s": {
2476
+ "verdict": "HIT",
2477
+ "predicted": "DOWN",
2478
+ "actual": "DOWN"
2479
+ },
2480
+ "n": {
2481
+ "verdict": "HIT",
2482
+ "predicted": "DOWN",
2483
+ "actual": "DOWN"
2484
+ }
2485
+ },
2486
+ "hits": 3,
2487
+ "total_calls": 3,
2488
+ "context": "Start Looking for Dow and S&P MACD SELL Signal on April 1"
2489
+ },
2490
+ "2026-03-30": {
2491
+ "actual": {
2492
+ "dji": 45216.14,
2493
+ "sp500": 6343.72,
2494
+ "nasdaq": 20794.64
2495
+ },
2496
+ "prev_close": {
2497
+ "dji": 45166.64,
2498
+ "sp500": 6368.85,
2499
+ "nasdaq": 20948.36
2500
+ },
2501
+ "pct_change": {
2502
+ "dji": 0.001096,
2503
+ "sp500": -0.003946,
2504
+ "nasdaq": -0.007338
2505
+ },
2506
+ "almanac_scores": {
2507
+ "d": 66.7,
2508
+ "s": 61.9,
2509
+ "n": 71.4
2510
+ },
2511
+ "results": {
2512
+ "d": {
2513
+ "verdict": "HIT",
2514
+ "predicted": "UP",
2515
+ "actual": "UP"
2516
+ },
2517
+ "s": {
2518
+ "verdict": "MISS",
2519
+ "predicted": "UP",
2520
+ "actual": "DOWN"
2521
+ },
2522
+ "n": {
2523
+ "verdict": "MISS",
2524
+ "predicted": "UP",
2525
+ "actual": "DOWN"
2526
+ }
2527
+ },
2528
+ "hits": 1,
2529
+ "total_calls": 3,
2530
+ "context": ""
2531
+ },
2532
+ "2026-03-31": {
2533
+ "actual": {
2534
+ "dji": 46341.51,
2535
+ "sp500": 6528.52,
2536
+ "nasdaq": 21590.63
2537
+ },
2538
+ "prev_close": {
2539
+ "dji": 45216.14,
2540
+ "sp500": 6343.72,
2541
+ "nasdaq": 20794.64
2542
+ },
2543
+ "pct_change": {
2544
+ "dji": 0.024889,
2545
+ "sp500": 0.029131,
2546
+ "nasdaq": 0.038279
2547
+ },
2548
+ "almanac_scores": {
2549
+ "d": 47.6,
2550
+ "s": 47.6,
2551
+ "n": 52.4
2552
+ },
2553
+ "results": {
2554
+ "d": {
2555
+ "verdict": "MISS",
2556
+ "predicted": "DOWN",
2557
+ "actual": "UP"
2558
+ },
2559
+ "s": {
2560
+ "verdict": "MISS",
2561
+ "predicted": "DOWN",
2562
+ "actual": "UP"
2563
+ },
2564
+ "n": {
2565
+ "verdict": "HIT",
2566
+ "predicted": "UP",
2567
+ "actual": "UP"
2568
+ }
2569
+ },
2570
+ "hits": 1,
2571
+ "total_calls": 3,
2572
+ "context": "Last Day of March, Dow Down 21 of Last 36, Russell 2000 Up 26 of Last 36"
2573
+ },
2574
+ "2026-04-01": {
2575
+ "actual": {
2576
+ "dji": 46565.74,
2577
+ "sp500": 6575.32,
2578
+ "nasdaq": 21840.95
2579
+ },
2580
+ "prev_close": {
2581
+ "dji": 46341.51,
2582
+ "sp500": 6528.52,
2583
+ "nasdaq": 21590.63
2584
+ },
2585
+ "pct_change": {
2586
+ "dji": 0.004839,
2587
+ "sp500": 0.007169,
2588
+ "nasdaq": 0.011594
2589
+ },
2590
+ "almanac_scores": {
2591
+ "d": 66.7,
2592
+ "s": 66.7,
2593
+ "n": 61.9
2594
+ },
2595
+ "results": {
2596
+ "d": {
2597
+ "verdict": "HIT",
2598
+ "predicted": "UP",
2599
+ "actual": "UP"
2600
+ },
2601
+ "s": {
2602
+ "verdict": "HIT",
2603
+ "predicted": "UP",
2604
+ "actual": "UP"
2605
+ },
2606
+ "n": {
2607
+ "verdict": "HIT",
2608
+ "predicted": "UP",
2609
+ "actual": "UP"
2610
+ }
2611
+ },
2612
+ "hits": 3,
2613
+ "total_calls": 3,
2614
+ "context": "First Trading Day in April, S&P Up 22 of Last 31"
2615
+ },
2616
+ "2026-04-02": {
2617
+ "actual": {
2618
+ "dji": 46504.67,
2619
+ "sp500": 6582.69,
2620
+ "nasdaq": 21879.18
2621
+ },
2622
+ "prev_close": {
2623
+ "dji": 46565.74,
2624
+ "sp500": 6575.32,
2625
+ "nasdaq": 21840.95
2626
+ },
2627
+ "pct_change": {
2628
+ "dji": -0.001311,
2629
+ "sp500": 0.001121,
2630
+ "nasdaq": 0.00175
2631
+ },
2632
+ "almanac_scores": {
2633
+ "d": 71.4,
2634
+ "s": 76.2,
2635
+ "n": 71.4
2636
+ },
2637
+ "results": {
2638
+ "d": {
2639
+ "verdict": "MISS",
2640
+ "predicted": "UP",
2641
+ "actual": "DOWN"
2642
+ },
2643
+ "s": {
2644
+ "verdict": "HIT",
2645
+ "predicted": "UP",
2646
+ "actual": "UP"
2647
+ },
2648
+ "n": {
2649
+ "verdict": "HIT",
2650
+ "predicted": "UP",
2651
+ "actual": "UP"
2652
+ }
2653
+ },
2654
+ "hits": 2,
2655
+ "total_calls": 3,
2656
+ "context": "Passover; NASDAQ Up 21 of Last 25 Days Before Good Friday"
2657
+ }
2658
+ },
2659
+ "weekly": {
2660
+ "2026-W00": {
2661
+ "hits": 2,
2662
+ "total_calls": 3,
2663
+ "accuracy": 66.7,
2664
+ "dates": [
2665
+ "2026-01-02"
2666
+ ],
2667
+ "dow": {
2668
+ "hits": 1,
2669
+ "total": 1,
2670
+ "pct": 100.0
2671
+ },
2672
+ "sp500": {
2673
+ "hits": 1,
2674
+ "total": 1,
2675
+ "pct": 100.0
2676
+ },
2677
+ "nasdaq": {
2678
+ "hits": 0,
2679
+ "total": 1,
2680
+ "pct": 0.0
2681
+ }
2682
+ },
2683
+ "2026-W01": {
2684
+ "hits": 7,
2685
+ "total_calls": 15,
2686
+ "accuracy": 46.7,
2687
+ "dates": [
2688
+ "2026-01-05",
2689
+ "2026-01-06",
2690
+ "2026-01-07",
2691
+ "2026-01-08",
2692
+ "2026-01-09"
2693
+ ],
2694
+ "dow": {
2695
+ "hits": 2,
2696
+ "total": 5,
2697
+ "pct": 40.0
2698
+ },
2699
+ "sp500": {
2700
+ "hits": 3,
2701
+ "total": 5,
2702
+ "pct": 60.0
2703
+ },
2704
+ "nasdaq": {
2705
+ "hits": 2,
2706
+ "total": 5,
2707
+ "pct": 40.0
2708
+ }
2709
+ },
2710
+ "2026-W02": {
2711
+ "hits": 5,
2712
+ "total_calls": 15,
2713
+ "accuracy": 33.3,
2714
+ "dates": [
2715
+ "2026-01-12",
2716
+ "2026-01-13",
2717
+ "2026-01-14",
2718
+ "2026-01-15",
2719
+ "2026-01-16"
2720
+ ],
2721
+ "dow": {
2722
+ "hits": 2,
2723
+ "total": 5,
2724
+ "pct": 40.0
2725
+ },
2726
+ "sp500": {
2727
+ "hits": 1,
2728
+ "total": 5,
2729
+ "pct": 20.0
2730
+ },
2731
+ "nasdaq": {
2732
+ "hits": 2,
2733
+ "total": 5,
2734
+ "pct": 40.0
2735
+ }
2736
+ },
2737
+ "2026-W03": {
2738
+ "hits": 10,
2739
+ "total_calls": 12,
2740
+ "accuracy": 83.3,
2741
+ "dates": [
2742
+ "2026-01-20",
2743
+ "2026-01-21",
2744
+ "2026-01-22",
2745
+ "2026-01-23"
2746
+ ],
2747
+ "dow": {
2748
+ "hits": 3,
2749
+ "total": 4,
2750
+ "pct": 75.0
2751
+ },
2752
+ "sp500": {
2753
+ "hits": 4,
2754
+ "total": 4,
2755
+ "pct": 100.0
2756
+ },
2757
+ "nasdaq": {
2758
+ "hits": 3,
2759
+ "total": 4,
2760
+ "pct": 75.0
2761
+ }
2762
+ },
2763
+ "2026-W04": {
2764
+ "hits": 11,
2765
+ "total_calls": 15,
2766
+ "accuracy": 73.3,
2767
+ "dates": [
2768
+ "2026-01-26",
2769
+ "2026-01-27",
2770
+ "2026-01-28",
2771
+ "2026-01-29",
2772
+ "2026-01-30"
2773
+ ],
2774
+ "dow": {
2775
+ "hits": 3,
2776
+ "total": 5,
2777
+ "pct": 60.0
2778
+ },
2779
+ "sp500": {
2780
+ "hits": 4,
2781
+ "total": 5,
2782
+ "pct": 80.0
2783
+ },
2784
+ "nasdaq": {
2785
+ "hits": 4,
2786
+ "total": 5,
2787
+ "pct": 80.0
2788
+ }
2789
+ },
2790
+ "2026-W05": {
2791
+ "hits": 8,
2792
+ "total_calls": 15,
2793
+ "accuracy": 53.3,
2794
+ "dates": [
2795
+ "2026-02-02",
2796
+ "2026-02-03",
2797
+ "2026-02-04",
2798
+ "2026-02-05",
2799
+ "2026-02-06"
2800
+ ],
2801
+ "dow": {
2802
+ "hits": 2,
2803
+ "total": 5,
2804
+ "pct": 40.0
2805
+ },
2806
+ "sp500": {
2807
+ "hits": 3,
2808
+ "total": 5,
2809
+ "pct": 60.0
2810
+ },
2811
+ "nasdaq": {
2812
+ "hits": 3,
2813
+ "total": 5,
2814
+ "pct": 60.0
2815
+ }
2816
+ },
2817
+ "2026-W06": {
2818
+ "hits": 4,
2819
+ "total_calls": 15,
2820
+ "accuracy": 26.7,
2821
+ "dates": [
2822
+ "2026-02-09",
2823
+ "2026-02-10",
2824
+ "2026-02-11",
2825
+ "2026-02-12",
2826
+ "2026-02-13"
2827
+ ],
2828
+ "dow": {
2829
+ "hits": 1,
2830
+ "total": 5,
2831
+ "pct": 20.0
2832
+ },
2833
+ "sp500": {
2834
+ "hits": 2,
2835
+ "total": 5,
2836
+ "pct": 40.0
2837
+ },
2838
+ "nasdaq": {
2839
+ "hits": 1,
2840
+ "total": 5,
2841
+ "pct": 20.0
2842
+ }
2843
+ },
2844
+ "2026-W07": {
2845
+ "hits": 8,
2846
+ "total_calls": 12,
2847
+ "accuracy": 66.7,
2848
+ "dates": [
2849
+ "2026-02-17",
2850
+ "2026-02-18",
2851
+ "2026-02-19",
2852
+ "2026-02-20"
2853
+ ],
2854
+ "dow": {
2855
+ "hits": 3,
2856
+ "total": 4,
2857
+ "pct": 75.0
2858
+ },
2859
+ "sp500": {
2860
+ "hits": 3,
2861
+ "total": 4,
2862
+ "pct": 75.0
2863
+ },
2864
+ "nasdaq": {
2865
+ "hits": 2,
2866
+ "total": 4,
2867
+ "pct": 50.0
2868
+ }
2869
+ },
2870
+ "2026-W08": {
2871
+ "hits": 11,
2872
+ "total_calls": 15,
2873
+ "accuracy": 73.3,
2874
+ "dates": [
2875
+ "2026-02-23",
2876
+ "2026-02-24",
2877
+ "2026-02-25",
2878
+ "2026-02-26",
2879
+ "2026-02-27"
2880
+ ],
2881
+ "dow": {
2882
+ "hits": 3,
2883
+ "total": 5,
2884
+ "pct": 60.0
2885
+ },
2886
+ "sp500": {
2887
+ "hits": 5,
2888
+ "total": 5,
2889
+ "pct": 100.0
2890
+ },
2891
+ "nasdaq": {
2892
+ "hits": 3,
2893
+ "total": 5,
2894
+ "pct": 60.0
2895
+ }
2896
+ },
2897
+ "2026-W09": {
2898
+ "hits": 12,
2899
+ "total_calls": 15,
2900
+ "accuracy": 80.0,
2901
+ "dates": [
2902
+ "2026-03-02",
2903
+ "2026-03-03",
2904
+ "2026-03-04",
2905
+ "2026-03-05",
2906
+ "2026-03-06"
2907
+ ],
2908
+ "dow": {
2909
+ "hits": 4,
2910
+ "total": 5,
2911
+ "pct": 80.0
2912
+ },
2913
+ "sp500": {
2914
+ "hits": 4,
2915
+ "total": 5,
2916
+ "pct": 80.0
2917
+ },
2918
+ "nasdaq": {
2919
+ "hits": 4,
2920
+ "total": 5,
2921
+ "pct": 80.0
2922
+ }
2923
+ },
2924
+ "2026-W10": {
2925
+ "hits": 4,
2926
+ "total_calls": 15,
2927
+ "accuracy": 26.7,
2928
+ "dates": [
2929
+ "2026-03-09",
2930
+ "2026-03-10",
2931
+ "2026-03-11",
2932
+ "2026-03-12",
2933
+ "2026-03-13"
2934
+ ],
2935
+ "dow": {
2936
+ "hits": 1,
2937
+ "total": 5,
2938
+ "pct": 20.0
2939
+ },
2940
+ "sp500": {
2941
+ "hits": 1,
2942
+ "total": 5,
2943
+ "pct": 20.0
2944
+ },
2945
+ "nasdaq": {
2946
+ "hits": 2,
2947
+ "total": 5,
2948
+ "pct": 40.0
2949
+ }
2950
+ },
2951
+ "2026-W11": {
2952
+ "hits": 4,
2953
+ "total_calls": 15,
2954
+ "accuracy": 26.7,
2955
+ "dates": [
2956
+ "2026-03-16",
2957
+ "2026-03-17",
2958
+ "2026-03-18",
2959
+ "2026-03-19",
2960
+ "2026-03-20"
2961
+ ],
2962
+ "dow": {
2963
+ "hits": 2,
2964
+ "total": 5,
2965
+ "pct": 40.0
2966
+ },
2967
+ "sp500": {
2968
+ "hits": 1,
2969
+ "total": 5,
2970
+ "pct": 20.0
2971
+ },
2972
+ "nasdaq": {
2973
+ "hits": 1,
2974
+ "total": 5,
2975
+ "pct": 20.0
2976
+ }
2977
+ },
2978
+ "2026-W12": {
2979
+ "hits": 11,
2980
+ "total_calls": 15,
2981
+ "accuracy": 73.3,
2982
+ "dates": [
2983
+ "2026-03-23",
2984
+ "2026-03-24",
2985
+ "2026-03-25",
2986
+ "2026-03-26",
2987
+ "2026-03-27"
2988
+ ],
2989
+ "dow": {
2990
+ "hits": 4,
2991
+ "total": 5,
2992
+ "pct": 80.0
2993
+ },
2994
+ "sp500": {
2995
+ "hits": 4,
2996
+ "total": 5,
2997
+ "pct": 80.0
2998
+ },
2999
+ "nasdaq": {
3000
+ "hits": 3,
3001
+ "total": 5,
3002
+ "pct": 60.0
3003
+ }
3004
+ },
3005
+ "2026-W13": {
3006
+ "hits": 7,
3007
+ "total_calls": 12,
3008
+ "accuracy": 58.3,
3009
+ "dates": [
3010
+ "2026-03-30",
3011
+ "2026-03-31",
3012
+ "2026-04-01",
3013
+ "2026-04-02"
3014
+ ],
3015
+ "dow": {
3016
+ "hits": 2,
3017
+ "total": 4,
3018
+ "pct": 50.0
3019
+ },
3020
+ "sp500": {
3021
+ "hits": 2,
3022
+ "total": 4,
3023
+ "pct": 50.0
3024
+ },
3025
+ "nasdaq": {
3026
+ "hits": 3,
3027
+ "total": 4,
3028
+ "pct": 75.0
3029
+ }
3030
+ }
3031
+ },
3032
+ "monthly": {
3033
+ "2026-01": {
3034
+ "hits": 35,
3035
+ "total_calls": 60,
3036
+ "accuracy": 58.3,
3037
+ "dow": {
3038
+ "hits": 11,
3039
+ "total": 20,
3040
+ "pct": 55.0
3041
+ },
3042
+ "sp500": {
3043
+ "hits": 13,
3044
+ "total": 20,
3045
+ "pct": 65.0
3046
+ },
3047
+ "nasdaq": {
3048
+ "hits": 11,
3049
+ "total": 20,
3050
+ "pct": 55.0
3051
+ },
3052
+ "trading_days": 20
3053
+ },
3054
+ "2026-02": {
3055
+ "hits": 31,
3056
+ "total_calls": 57,
3057
+ "accuracy": 54.4,
3058
+ "dow": {
3059
+ "hits": 9,
3060
+ "total": 19,
3061
+ "pct": 47.4
3062
+ },
3063
+ "sp500": {
3064
+ "hits": 13,
3065
+ "total": 19,
3066
+ "pct": 68.4
3067
+ },
3068
+ "nasdaq": {
3069
+ "hits": 9,
3070
+ "total": 19,
3071
+ "pct": 47.4
3072
+ },
3073
+ "trading_days": 19
3074
+ },
3075
+ "2026-03": {
3076
+ "hits": 33,
3077
+ "total_calls": 66,
3078
+ "accuracy": 50.0,
3079
+ "dow": {
3080
+ "hits": 12,
3081
+ "total": 22,
3082
+ "pct": 54.5
3083
+ },
3084
+ "sp500": {
3085
+ "hits": 10,
3086
+ "total": 22,
3087
+ "pct": 45.5
3088
+ },
3089
+ "nasdaq": {
3090
+ "hits": 11,
3091
+ "total": 22,
3092
+ "pct": 50.0
3093
+ },
3094
+ "trading_days": 22
3095
+ },
3096
+ "2026-04": {
3097
+ "hits": 5,
3098
+ "total_calls": 6,
3099
+ "accuracy": 83.3,
3100
+ "dow": {
3101
+ "hits": 1,
3102
+ "total": 2,
3103
+ "pct": 50.0
3104
+ },
3105
+ "sp500": {
3106
+ "hits": 2,
3107
+ "total": 2,
3108
+ "pct": 100.0
3109
+ },
3110
+ "nasdaq": {
3111
+ "hits": 2,
3112
+ "total": 2,
3113
+ "pct": 100.0
3114
+ },
3115
+ "trading_days": 2
3116
+ }
3117
+ }
3118
+ }
tests/test_almanac_accuracy_api.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the Almanac historic accuracy API routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import sys
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
14
+
15
+ import app as app_module
16
+
17
+
18
+ ACCURACY_FIXTURE = {
19
+ "meta": {
20
+ "last_updated": "2026-04-05T12:00:00Z",
21
+ "total_days_scored": 3,
22
+ "data_range": {"from": "2026-01-02", "to": "2026-01-06"},
23
+ "source": "Historic CSV backtest via scripts/seed_accuracy.py",
24
+ },
25
+ "daily": {
26
+ "2026-01-02": {
27
+ "actual": {"dji": 101.0, "sp500": 199.0, "nasdaq": 300.0},
28
+ "prev_close": {"dji": 100.0, "sp500": 200.0, "nasdaq": 300.0},
29
+ "pct_change": {"dji": 0.01, "sp500": -0.005, "nasdaq": 0.0},
30
+ "almanac_scores": {"d": 60.0, "s": 40.0, "n": 50.0},
31
+ "results": {
32
+ "d": {"verdict": "HIT", "predicted": "UP", "actual": "UP"},
33
+ "s": {"verdict": "HIT", "predicted": "DOWN", "actual": "DOWN"},
34
+ "n": {"verdict": None, "predicted": None, "actual": "FLAT"},
35
+ },
36
+ "hits": 2,
37
+ "total_calls": 2,
38
+ "context": "Opening session",
39
+ },
40
+ "2026-01-05": {
41
+ "actual": {"dji": 100.0, "sp500": 200.0, "nasdaq": 303.0},
42
+ "prev_close": {"dji": 101.0, "sp500": 199.0, "nasdaq": 300.0},
43
+ "pct_change": {"dji": -0.009901, "sp500": 0.005025, "nasdaq": 0.01},
44
+ "almanac_scores": {"d": 45.0, "s": 55.0, "n": 70.0},
45
+ "results": {
46
+ "d": {"verdict": "HIT", "predicted": "DOWN", "actual": "DOWN"},
47
+ "s": {"verdict": "HIT", "predicted": "UP", "actual": "UP"},
48
+ "n": {"verdict": "HIT", "predicted": "UP", "actual": "UP"},
49
+ },
50
+ "hits": 3,
51
+ "total_calls": 3,
52
+ "context": "",
53
+ },
54
+ "2026-01-06": {
55
+ "actual": {"dji": 102.0, "sp500": 198.0, "nasdaq": 300.0},
56
+ "prev_close": {"dji": 100.0, "sp500": 200.0, "nasdaq": 303.0},
57
+ "pct_change": {"dji": 0.02, "sp500": -0.01, "nasdaq": -0.009901},
58
+ "almanac_scores": {"d": 80.0, "s": 20.0, "n": 60.0},
59
+ "results": {
60
+ "d": {"verdict": "HIT", "predicted": "UP", "actual": "UP"},
61
+ "s": {"verdict": "HIT", "predicted": "DOWN", "actual": "DOWN"},
62
+ "n": {"verdict": "MISS", "predicted": "UP", "actual": "DOWN"},
63
+ },
64
+ "hits": 2,
65
+ "total_calls": 3,
66
+ "context": "Momentum test",
67
+ },
68
+ },
69
+ "weekly": {
70
+ "2026-W00": {
71
+ "dates": ["2026-01-02"],
72
+ "hits": 2,
73
+ "total_calls": 2,
74
+ "accuracy": 100.0,
75
+ "dow": {"hits": 1, "total": 1, "pct": 100.0},
76
+ "sp500": {"hits": 1, "total": 1, "pct": 100.0},
77
+ "nasdaq": {"hits": 0, "total": 0, "pct": 0.0},
78
+ },
79
+ "2026-W01": {
80
+ "dates": ["2026-01-05", "2026-01-06"],
81
+ "hits": 5,
82
+ "total_calls": 6,
83
+ "accuracy": 83.3,
84
+ "dow": {"hits": 2, "total": 2, "pct": 100.0},
85
+ "sp500": {"hits": 2, "total": 2, "pct": 100.0},
86
+ "nasdaq": {"hits": 1, "total": 2, "pct": 50.0},
87
+ },
88
+ },
89
+ "monthly": {
90
+ "2026-01": {
91
+ "hits": 7,
92
+ "total_calls": 8,
93
+ "accuracy": 87.5,
94
+ "dow": {"hits": 3, "total": 3, "pct": 100.0},
95
+ "sp500": {"hits": 3, "total": 3, "pct": 100.0},
96
+ "nasdaq": {"hits": 1, "total": 2, "pct": 50.0},
97
+ "trading_days": 3,
98
+ }
99
+ },
100
+ }
101
+
102
+
103
+ class TestAlmanacAccuracyAPI(unittest.TestCase):
104
+ def setUp(self):
105
+ app_module.app.config["TESTING"] = True
106
+ self.client = app_module.app.test_client()
107
+ app_module._accuracy_data = None
108
+ app_module._accuracy_mtime = 0.0
109
+
110
+ def tearDown(self):
111
+ app_module._accuracy_data = None
112
+ app_module._accuracy_mtime = 0.0
113
+
114
+ def make_root(self, name: str) -> Path:
115
+ root = Path(__file__).resolve().parent.parent / "tmp_feedback_test_main" / name
116
+ shutil.rmtree(root, ignore_errors=True)
117
+ root.mkdir(parents=True, exist_ok=True)
118
+ return root
119
+
120
+ def write_accuracy_fixture(self, root: Path) -> None:
121
+ path = root / "data" / "almanac_2026" / "accuracy_results.json"
122
+ path.parent.mkdir(parents=True, exist_ok=True)
123
+ path.write_text(json.dumps(ACCURACY_FIXTURE, indent=2), encoding="utf-8")
124
+
125
+ def test_accuracy_endpoint_reports_unavailable_when_seed_file_is_missing(self):
126
+ temp_root = self.make_root("accuracy_api_unavailable")
127
+ try:
128
+ with patch.object(app_module, "PROJECT_ROOT", temp_root):
129
+ resp = self.client.get("/api/almanac/accuracy")
130
+ finally:
131
+ shutil.rmtree(temp_root, ignore_errors=True)
132
+
133
+ self.assertEqual(resp.status_code, 200)
134
+ data = resp.get_json()
135
+ self.assertEqual(data["available"], False)
136
+ self.assertIn("seed_accuracy.py", data["message"])
137
+
138
+ def test_accuracy_endpoint_supports_all_single_date_and_range_queries(self):
139
+ temp_root = self.make_root("accuracy_api_daily")
140
+ self.write_accuracy_fixture(temp_root)
141
+ try:
142
+ with patch.object(app_module, "PROJECT_ROOT", temp_root):
143
+ all_resp = self.client.get("/api/almanac/accuracy")
144
+ day_resp = self.client.get("/api/almanac/accuracy?date=2026-01-06")
145
+ range_resp = self.client.get("/api/almanac/accuracy?from=2026-01-05&to=2026-01-06")
146
+ missing_resp = self.client.get("/api/almanac/accuracy?date=2026-01-07")
147
+ finally:
148
+ shutil.rmtree(temp_root, ignore_errors=True)
149
+
150
+ self.assertEqual(all_resp.status_code, 200)
151
+ self.assertIn("2026-01-02", all_resp.get_json()["daily"])
152
+ self.assertEqual(day_resp.status_code, 200)
153
+ self.assertEqual(day_resp.get_json()["results"]["n"]["verdict"], "MISS")
154
+ self.assertEqual(range_resp.status_code, 200)
155
+ self.assertEqual(sorted(range_resp.get_json()["daily"].keys()), ["2026-01-05", "2026-01-06"])
156
+ self.assertEqual(missing_resp.status_code, 404)
157
+
158
+ def test_accuracy_week_and_month_routes_return_expected_records(self):
159
+ temp_root = self.make_root("accuracy_api_periods")
160
+ self.write_accuracy_fixture(temp_root)
161
+ try:
162
+ with patch.object(app_module, "PROJECT_ROOT", temp_root):
163
+ week_resp = self.client.get("/api/almanac/accuracy/week?start=2026-01-05")
164
+ month_resp = self.client.get("/api/almanac/accuracy/month?month=2026-01")
165
+ invalid_week_resp = self.client.get("/api/almanac/accuracy/week?start=not-a-date")
166
+ finally:
167
+ shutil.rmtree(temp_root, ignore_errors=True)
168
+
169
+ self.assertEqual(week_resp.status_code, 200)
170
+ self.assertEqual(week_resp.get_json()["hits"], 5)
171
+ self.assertEqual(month_resp.status_code, 200)
172
+ self.assertEqual(month_resp.get_json()["trading_days"], 3)
173
+ self.assertEqual(invalid_week_resp.status_code, 400)
174
+
175
+ def test_accuracy_summary_aggregates_monthly_and_per_index_totals(self):
176
+ temp_root = self.make_root("accuracy_api_summary")
177
+ self.write_accuracy_fixture(temp_root)
178
+ try:
179
+ with patch.object(app_module, "PROJECT_ROOT", temp_root):
180
+ resp = self.client.get("/api/almanac/accuracy/summary")
181
+ finally:
182
+ shutil.rmtree(temp_root, ignore_errors=True)
183
+
184
+ self.assertEqual(resp.status_code, 200)
185
+ data = resp.get_json()
186
+ self.assertEqual(data["overall"]["hits"], 7)
187
+ self.assertEqual(data["overall"]["total_calls"], 8)
188
+ self.assertEqual(data["overall"]["accuracy"], 87.5)
189
+ self.assertEqual(data["per_index"]["dow"]["pct"], 100.0)
190
+ self.assertEqual(data["per_index"]["nasdaq"]["pct"], 50.0)
191
+ self.assertEqual(data["last_scored_date"], "2026-01-06")
192
+ self.assertEqual(data["total_days"], 3)
193
+
194
+
195
+ if __name__ == "__main__":
196
+ unittest.main()
tests/test_almanac_iris_api.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the Almanac IRIS snapshot and refresh API routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import sys
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
17
+
18
+ import app as app_module
19
+ import iris_mvp
20
+
21
+
22
+ def build_report(
23
+ symbol: str,
24
+ *,
25
+ current_price: float,
26
+ predicted_price: float,
27
+ session_date: str,
28
+ generated_at: str,
29
+ trend_label: str = "STRONG UPTREND",
30
+ investment_signal: str = "BUY",
31
+ check_engine_light: str = " GREEN (Safe to Proceed)",
32
+ pct_change: float = 1.25,
33
+ direction: str = "upward",
34
+ ):
35
+ return {
36
+ "meta": {
37
+ "symbol": symbol,
38
+ "source_symbol": symbol,
39
+ "generated_at": generated_at,
40
+ "market_session_date": session_date,
41
+ "horizon_days": 1,
42
+ },
43
+ "market": {
44
+ "current_price": current_price,
45
+ "predicted_price_next_session": predicted_price,
46
+ "predicted_price_horizon": predicted_price,
47
+ },
48
+ "signals": {
49
+ "trend_label": trend_label,
50
+ "investment_signal": investment_signal,
51
+ "check_engine_light": check_engine_light,
52
+ "sentiment_score": 0.0,
53
+ "iris_reasoning": {
54
+ "pct_change": pct_change,
55
+ "direction": direction,
56
+ "top_factors": ["SMA(10)", "Day Trend", "SMA(20)"],
57
+ },
58
+ "model_confidence": 92.5,
59
+ },
60
+ "all_horizons": {
61
+ "1D": {
62
+ "predicted_price": predicted_price,
63
+ "trend_label": trend_label,
64
+ "investment_signal": investment_signal,
65
+ "iris_reasoning": {
66
+ "pct_change": pct_change,
67
+ "direction": direction,
68
+ "top_factors": ["SMA(10)", "Day Trend", "SMA(20)"],
69
+ },
70
+ "model_confidence": 92.5,
71
+ }
72
+ },
73
+ }
74
+
75
+
76
+ class DummyModel:
77
+ feature_importances_ = np.array([0.4, 0.3, 0.2, 0.1])
78
+
79
+
80
+ class FakeIrisApp:
81
+ def get_market_data(self, ticker):
82
+ history_df = pd.DataFrame(
83
+ {"rsi_14": [48.0, 56.0]},
84
+ index=pd.to_datetime(["2026-04-01", "2026-04-02"]),
85
+ )
86
+ base_prices = {
87
+ "SPY": 650.0,
88
+ "^DJI": 46000.0,
89
+ "^GSPC": 6550.0,
90
+ "^IXIC": 21800.0,
91
+ }
92
+ return {
93
+ "current_price": base_prices[ticker],
94
+ "history_df": history_df,
95
+ }
96
+
97
+ def predict_trend(self, data, sentiment_score, horizon_days=1):
98
+ current_price = float(data["current_price"])
99
+ predicted_price = current_price * 1.01
100
+ return (
101
+ "STRONG UPTREND",
102
+ predicted_price,
103
+ [predicted_price],
104
+ [predicted_price * 1.02],
105
+ [predicted_price * 0.98],
106
+ DummyModel(),
107
+ 88.4,
108
+ )
109
+
110
+
111
+ class TestAlmanacIrisAPI(unittest.TestCase):
112
+ def setUp(self):
113
+ app_module.app.config["TESTING"] = True
114
+ self.client = app_module.app.test_client()
115
+ self.original_data_dir = app_module.DATA_DIR
116
+ self.original_iris_app = app_module.iris_app
117
+ app_module._iris_snapshot_cache = {"data": None, "ts": 0.0}
118
+
119
+ def tearDown(self):
120
+ app_module.DATA_DIR = self.original_data_dir
121
+ app_module.iris_app = self.original_iris_app
122
+ app_module._iris_snapshot_cache = {"data": None, "ts": 0.0}
123
+
124
+ def make_root(self, name: str) -> Path:
125
+ root = Path(__file__).resolve().parent.parent / "tmp_feedback_test_main" / name
126
+ shutil.rmtree(root, ignore_errors=True)
127
+ root.mkdir(parents=True, exist_ok=True)
128
+ return root
129
+
130
+ def test_iris_snapshot_reads_latest_valid_reports_from_data_dir(self):
131
+ temp_root = self.make_root("almanac_iris_snapshot")
132
+ try:
133
+ (temp_root / "SPY_report.json").write_text(
134
+ json.dumps(
135
+ [
136
+ build_report(
137
+ "SPY",
138
+ current_price=640.0,
139
+ predicted_price=646.4,
140
+ session_date="2026-04-01",
141
+ generated_at="2026-04-01T12:00:00Z",
142
+ )
143
+ ],
144
+ indent=2,
145
+ ),
146
+ encoding="utf-8",
147
+ )
148
+ # The latest entry is intentionally invalid due to low price and must be skipped.
149
+ (temp_root / "^DJI_report.json").write_text(
150
+ json.dumps(
151
+ [
152
+ build_report(
153
+ "^DJI",
154
+ current_price=46200.0,
155
+ predicted_price=46350.0,
156
+ session_date="2026-04-02",
157
+ generated_at="2026-04-02T12:00:00Z",
158
+ investment_signal="BUY",
159
+ ),
160
+ build_report(
161
+ "DJI",
162
+ current_price=88.0,
163
+ predicted_price=91.0,
164
+ session_date="2026-04-03",
165
+ generated_at="2026-04-03T12:00:00Z",
166
+ investment_signal="BUY",
167
+ ),
168
+ ],
169
+ indent=2,
170
+ ),
171
+ encoding="utf-8",
172
+ )
173
+ (temp_root / "^IXIC_report.json").write_text(
174
+ json.dumps(
175
+ build_report(
176
+ "^IXIC",
177
+ current_price=21850.0,
178
+ predicted_price=21960.0,
179
+ session_date="2026-04-02",
180
+ generated_at="2026-04-02T11:00:00Z",
181
+ investment_signal="SELL",
182
+ trend_label="STRONG DOWNTREND",
183
+ check_engine_light=" RED (Risk Detected - Caution)",
184
+ pct_change=-0.5,
185
+ direction="downward",
186
+ ),
187
+ indent=2,
188
+ ),
189
+ encoding="utf-8",
190
+ )
191
+
192
+ with patch.object(app_module, "DATA_DIR", temp_root):
193
+ resp = self.client.get("/api/almanac/iris-snapshot")
194
+ finally:
195
+ shutil.rmtree(temp_root, ignore_errors=True)
196
+
197
+ self.assertEqual(resp.status_code, 200)
198
+ data = resp.get_json()
199
+ self.assertTrue(data["indices"]["spy"]["available"])
200
+ self.assertTrue(data["indices"]["dji"]["available"])
201
+ self.assertEqual(data["indices"]["dji"]["session_date"], "2026-04-02")
202
+ self.assertFalse(data["indices"]["gspc"]["available"])
203
+ self.assertEqual(data["indices"]["ixic"]["direction"], "downward")
204
+ self.assertEqual(data["indices"]["ixic"]["investment_signal"], "SELL")
205
+
206
+ def test_iris_refresh_returns_lightweight_predictions_for_all_indices(self):
207
+ app_module.iris_app = FakeIrisApp()
208
+ app_module._iris_snapshot_cache = {"data": {"stale": True}, "ts": 123.0}
209
+
210
+ resp = self.client.get("/api/almanac/iris-refresh")
211
+
212
+ self.assertEqual(resp.status_code, 200)
213
+ data = resp.get_json()
214
+ self.assertEqual(sorted(data["indices"].keys()), ["dji", "gspc", "ixic", "spy"])
215
+ self.assertTrue(all(entry["available"] for entry in data["indices"].values()))
216
+ self.assertEqual(data["indices"]["dji"]["direction"], "upward")
217
+ self.assertEqual(data["indices"]["gspc"]["investment_signal"], "STRONG BUY")
218
+ self.assertEqual(data["indices"]["ixic"]["source"], "live_rf_prediction")
219
+ self.assertEqual(app_module._iris_snapshot_cache["data"], None)
220
+ self.assertEqual(app_module._iris_snapshot_cache["ts"], 0.0)
221
+
222
+ def test_default_tickers_include_spy_and_three_indices(self):
223
+ for ticker in ("SPY", "^DJI", "^GSPC", "^IXIC"):
224
+ self.assertIn(ticker, iris_mvp.DEFAULT_TICKERS)
225
+
226
+
227
+ if __name__ == "__main__":
228
+ unittest.main()