Trebor1009 commited on
Commit
0465c7e
·
verified ·
1 Parent(s): 19a91cb

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +102 -5
src/streamlit_app.py CHANGED
@@ -8,6 +8,7 @@ import pandas as pd
8
  import numpy as np
9
  import matplotlib.pyplot as plt
10
  from numpy.lib.stride_tricks import sliding_window_view
 
11
  from sklearn.ensemble import GradientBoostingClassifier
12
  from huggingface_hub import HfApi, hf_hub_download
13
 
@@ -725,6 +726,91 @@ def render_sniper_tab():
725
  render_sniper_dashboard(pick, result)
726
 
727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728
  # =========================================================
729
  # TAB 4: SIGNAL TRACKER (predicted vs. actual, out-of-sample)
730
  # =========================================================
@@ -829,10 +915,21 @@ def render_signal_tracker():
829
  ax.axvline(flag_date, color="red", ls="-", alpha=0.4, label="Flagged here")
830
  if pd.notna(flag_price):
831
  ax.axhline(flag_price, color="gray", ls=":", alpha=0.6, label="Price at flag")
832
- if pd.notna(row.get("Avg_3M")):
833
- predicted_price = flag_price * (1 + row["Avg_3M"] / 100)
834
- ax.axhline(predicted_price, color="green", ls="--", alpha=0.7,
835
- label="Predicted avg 3M level")
 
 
 
 
 
 
 
 
 
 
 
836
  ax.set_title(f"{ticker} — flagged {flag_date.date()}")
837
  ax.legend(loc="best", fontsize=9)
838
  ax.grid(True, alpha=0.1)
@@ -858,4 +955,4 @@ with tab3:
858
  render_sniper_tab()
859
 
860
  with tab4:
861
- render_signal_tracker()
 
8
  import numpy as np
9
  import matplotlib.pyplot as plt
10
  from numpy.lib.stride_tricks import sliding_window_view
11
+ from scipy.stats import spearmanr
12
  from sklearn.ensemble import GradientBoostingClassifier
13
  from huggingface_hub import HfApi, hf_hub_download
14
 
 
726
  render_sniper_dashboard(pick, result)
727
 
728
 
729
+ # =========================================================
730
+ # ANALOG PATH PROJECTION (used by the Signal Tracker chart)
731
+ # =========================================================
732
+
733
+ ANALOG_LOOKBACK = 60
734
+ ANALOG_FORWARD = 60
735
+ ANALOG_TOP_N = 5
736
+ ANALOG_WEIGHT_STRENGTH = 5
737
+
738
+
739
+ def _similarity_matches(close, end_idx, lookback, forward):
740
+ if end_idx - lookback < 0:
741
+ return []
742
+ recent = close.iloc[end_idx - lookback:end_idx]
743
+ recent_norm = (recent / recent.iloc[0]).values.flatten()
744
+
745
+ matches = []
746
+ max_search = end_idx - lookback - forward
747
+ for i in range(max_search):
748
+ cand = close.iloc[i:i + lookback]
749
+ cand_norm = (cand / cand.iloc[0]).values.flatten()
750
+ corr = spearmanr(recent_norm, cand_norm)[0]
751
+ if not np.isnan(corr):
752
+ matches.append((float(corr), i))
753
+ return matches
754
+
755
+
756
+ @st.cache_data(ttl=24 * 3600, show_spinner="Building analog path projection...")
757
+ def compute_analog_projection(ticker: str, flag_date_str: str, lookback=ANALOG_LOOKBACK,
758
+ forward=ANALOG_FORWARD, top_n=ANALOG_TOP_N,
759
+ weight_strength=ANALOG_WEIGHT_STRENGTH):
760
+ hist = get_sniper_ticker_data(ticker)
761
+ if hist.empty:
762
+ return None
763
+ close = hist["Close"]
764
+ flag_date = pd.Timestamp(flag_date_str)
765
+ end_idx = int(close.index.searchsorted(flag_date))
766
+ end_idx = min(end_idx, len(close) - 1)
767
+
768
+ if end_idx - lookback - forward < 5:
769
+ return None # not enough pre-flag history for this method
770
+
771
+ matches = _similarity_matches(close, end_idx, lookback, forward)
772
+ if not matches:
773
+ return None
774
+
775
+ matches.sort(key=lambda x: x[0], reverse=True)
776
+ filtered, used = [], []
777
+ for corr, idx in matches:
778
+ if all(abs(idx - u) >= lookback for u in used):
779
+ filtered.append((corr, idx))
780
+ used.append(idx)
781
+ if len(filtered) >= top_n:
782
+ break
783
+ if not filtered:
784
+ return None
785
+
786
+ corrs = np.array([c for c, _ in filtered])
787
+ weights = np.exp(weight_strength * corrs)
788
+ weights /= weights.sum()
789
+
790
+ anchor_price = close.iloc[end_idx]
791
+ projections = []
792
+ for corr, idx in filtered:
793
+ fwd = close.iloc[idx + lookback: idx + lookback + forward]
794
+ fwd_norm = (fwd / fwd.iloc[0]).values.flatten()
795
+ projections.append(fwd_norm * anchor_price)
796
+ projections = np.vstack(projections)
797
+
798
+ weighted_mean = np.average(projections, axis=0, weights=weights)
799
+ weighted_var = np.average((projections - weighted_mean) ** 2, axis=0, weights=weights)
800
+ weighted_std = np.sqrt(weighted_var)
801
+
802
+ future_dates = pd.bdate_range(start=close.index[end_idx], periods=forward + 1)[1:]
803
+
804
+ return {
805
+ "future_dates": future_dates,
806
+ "weighted_mean": weighted_mean,
807
+ "upper": weighted_mean + weighted_std,
808
+ "lower": weighted_mean - weighted_std,
809
+ "robust_similarity": float(np.sqrt(np.sum(weights * corrs ** 2)) * 100),
810
+ "match_count": len(filtered),
811
+ }
812
+
813
+
814
  # =========================================================
815
  # TAB 4: SIGNAL TRACKER (predicted vs. actual, out-of-sample)
816
  # =========================================================
 
915
  ax.axvline(flag_date, color="red", ls="-", alpha=0.4, label="Flagged here")
916
  if pd.notna(flag_price):
917
  ax.axhline(flag_price, color="gray", ls=":", alpha=0.6, label="Price at flag")
918
+
919
+ projection = compute_analog_projection(ticker, str(flag_date.date()))
920
+ if projection:
921
+ ax.plot(projection["future_dates"], projection["weighted_mean"],
922
+ color="green", ls="--", linewidth=2, label="Analog-weighted projection")
923
+ ax.fill_between(projection["future_dates"], projection["lower"], projection["upper"],
924
+ color="green", alpha=0.15, label="Projection band (1σ)")
925
+ st.caption(
926
+ f"Projection built from the {projection['match_count']} most similar non-overlapping "
927
+ f"60-day windows in {ticker}'s own history, as known on the flag date "
928
+ f"(robust similarity: {projection['robust_similarity']:.1f}%). Read this as 'what "
929
+ f"comparable past setups did,' not a calibrated forecast — a handful of historical "
930
+ f"echoes, not independent confirming evidence."
931
+ )
932
+
933
  ax.set_title(f"{ticker} — flagged {flag_date.date()}")
934
  ax.legend(loc="best", fontsize=9)
935
  ax.grid(True, alpha=0.1)
 
955
  render_sniper_tab()
956
 
957
  with tab4:
958
+ render_signal_tracker()