Spaces:
Running
Running
Update src/streamlit_app.py
Browse files- 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 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|