Upload 6 files
Browse files- app.py +398 -0
- creds.json +13 -0
- histgb_pca_model_clean.pkl +3 -0
- pca.pkl +3 -0
- requirements.txt +13 -0
- scaler.pkl +3 -0
app.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app_final.py (final debugged version)
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import requests
|
| 4 |
+
import yfinance as yf
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import numpy as np
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
import joblib
|
| 10 |
+
import re
|
| 11 |
+
import time
|
| 12 |
+
|
| 13 |
+
# ---------------------------- CONFIG ----------------------------
|
| 14 |
+
HF_API_TOKEN = st.secrets["HF_API_TOKEN"]
|
| 15 |
+
CRYPTO_NEWS_API_KEY = "of9jvyylshwcddtw0qsv16zswpi8k39lbr67qm97"
|
| 16 |
+
FRED_API_KEY = "4c3fd5be0b1f052f5d1d0080261277b1"
|
| 17 |
+
|
| 18 |
+
FINBERT_API = "https://api-inference.huggingface.co/models/ProsusAI/finbert"
|
| 19 |
+
HEADERS = {"Authorization": f"Bearer {HF_API_TOKEN}"}
|
| 20 |
+
|
| 21 |
+
TICKERS = {
|
| 22 |
+
"bitcoin": "BTC-USD",
|
| 23 |
+
"gold": "GC=F",
|
| 24 |
+
"sp500": "^GSPC",
|
| 25 |
+
"dxy": "DX-Y.NYB"
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
FRED_CODES = {
|
| 29 |
+
"interest_rate": "FEDFUNDS",
|
| 30 |
+
"inflation": "CPIAUCSL"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
model = joblib.load("histgb_pca_model_clean.pkl")
|
| 34 |
+
pca = joblib.load("pca.pkl")
|
| 35 |
+
scaler = joblib.load("scaler.pkl")
|
| 36 |
+
|
| 37 |
+
# ---------------------------- FUNCTIONS ----------------------------
|
| 38 |
+
def fetch_news(source):
|
| 39 |
+
url = f"https://cryptonews-api.com/api/v1/category"
|
| 40 |
+
params = {
|
| 41 |
+
"section": "general",
|
| 42 |
+
"items": 10,
|
| 43 |
+
"page": 1,
|
| 44 |
+
"source": source,
|
| 45 |
+
"token": CRYPTO_NEWS_API_KEY
|
| 46 |
+
}
|
| 47 |
+
r = requests.get(url, params=params)
|
| 48 |
+
articles = r.json().get("data", [])
|
| 49 |
+
texts = []
|
| 50 |
+
for art in articles:
|
| 51 |
+
summary = art.get("text") or art.get("content", "").split(".")[0]
|
| 52 |
+
texts.append(summary)
|
| 53 |
+
return texts
|
| 54 |
+
|
| 55 |
+
def call_finbert(news_list):
|
| 56 |
+
results_df = []
|
| 57 |
+
news_list = news_list[:5]
|
| 58 |
+
for idx, news in enumerate(news_list):
|
| 59 |
+
if not isinstance(news, str) or not news.strip():
|
| 60 |
+
results_df.append({"positive": 0.0, "neutral": 0.0, "negative": 0.0})
|
| 61 |
+
continue
|
| 62 |
+
payload = {"inputs": news}
|
| 63 |
+
for attempt in range(5):
|
| 64 |
+
try:
|
| 65 |
+
response = requests.post(FINBERT_API, headers=HEADERS, json=payload, timeout=30)
|
| 66 |
+
response.raise_for_status()
|
| 67 |
+
output = response.json()
|
| 68 |
+
|
| 69 |
+
# Get raw scores
|
| 70 |
+
scores_raw = {item["label"].lower(): item["score"] for item in output[0]}
|
| 71 |
+
|
| 72 |
+
# Ensure fixed column order
|
| 73 |
+
aligned_scores = {
|
| 74 |
+
"positive": scores_raw.get("positive", 0.0),
|
| 75 |
+
"neutral": scores_raw.get("neutral", 0.0),
|
| 76 |
+
"negative": scores_raw.get("negative", 0.0)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
results_df.append(aligned_scores)
|
| 80 |
+
break
|
| 81 |
+
except requests.exceptions.RequestException as e:
|
| 82 |
+
st.warning(f"⚠️ FinBERT error on article {idx+1}, attempt {attempt+1}/5: {e}")
|
| 83 |
+
time.sleep(2)
|
| 84 |
+
except Exception as ex:
|
| 85 |
+
st.warning(f"❌ Failed to analyze article {idx+1}: {ex}")
|
| 86 |
+
results_df.append({"positive": 0.0, "neutral": 0.0, "negative": 0.0})
|
| 87 |
+
break
|
| 88 |
+
return pd.DataFrame(results_df)
|
| 89 |
+
|
| 90 |
+
def aggregate_sentiments(sentiment_df):
|
| 91 |
+
scaled = sentiment_df.copy()
|
| 92 |
+
for col in scaled.columns:
|
| 93 |
+
scaled[col] = (scaled[col] - scaled[col].min()) / (scaled[col].max() - scaled[col].min() + 1e-8)
|
| 94 |
+
weighted = scaled.copy()
|
| 95 |
+
for col in ["positive", "negative"]:
|
| 96 |
+
weighted[col] = np.where(scaled[col] > 0.75, scaled[col] * 1.5, scaled[col])
|
| 97 |
+
weighted[col] = np.clip(weighted[col], 0, 1)
|
| 98 |
+
weighted["neutral"] = scaled["neutral"]
|
| 99 |
+
return weighted.mean().to_dict(), (scaled > 0.75).sum().to_dict()
|
| 100 |
+
|
| 101 |
+
def fetch_yahoo_data(ticker, date):
|
| 102 |
+
data = yf.Ticker(ticker).history(start=date, end=date + timedelta(days=1))
|
| 103 |
+
if not data.empty:
|
| 104 |
+
return {
|
| 105 |
+
"open": round(data["Open"].iloc[0], 2),
|
| 106 |
+
"high": round(data["High"].iloc[0], 2),
|
| 107 |
+
"low": round(data["Low"].iloc[0], 2),
|
| 108 |
+
"close": round(data["Close"].iloc[0], 2),
|
| 109 |
+
"volume": int(data["Volume"].iloc[0]) if ticker != TICKERS["dxy"] else None,
|
| 110 |
+
"change_pct": round(((data["Close"].iloc[0] - data["Open"].iloc[0]) / data["Open"].iloc[0]) * 100, 2)
|
| 111 |
+
}
|
| 112 |
+
else:
|
| 113 |
+
st.warning(f"⚠️ No trading data for {ticker} on {date.strftime('%Y-%m-%d')}, using previous available data.")
|
| 114 |
+
return fetch_yahoo_data(ticker, date - timedelta(days=1))
|
| 115 |
+
|
| 116 |
+
def fetch_fred(code, month):
|
| 117 |
+
url = f"https://api.stlouisfed.org/fred/series/observations"
|
| 118 |
+
params = {
|
| 119 |
+
"series_id": code,
|
| 120 |
+
"observation_start": f"{month}-01",
|
| 121 |
+
"api_key": FRED_API_KEY,
|
| 122 |
+
"file_type": "json"
|
| 123 |
+
}
|
| 124 |
+
res = requests.get(url, params=params).json()
|
| 125 |
+
try:
|
| 126 |
+
return float(res["observations"][0]["value"])
|
| 127 |
+
except:
|
| 128 |
+
prev_month = (datetime.strptime(month, "%Y-%m") - timedelta(days=30)).strftime("%Y-%m")
|
| 129 |
+
return fetch_fred(code, prev_month)
|
| 130 |
+
|
| 131 |
+
def make_prediction(input_data):
|
| 132 |
+
expected_cols = list(scaler.feature_names_in_)
|
| 133 |
+
|
| 134 |
+
# SAFETY CHECK
|
| 135 |
+
if len(input_data) != len(expected_cols):
|
| 136 |
+
raise ValueError(f"❌ Input length mismatch! Got {len(input_data)}, expected {len(expected_cols)}")
|
| 137 |
+
|
| 138 |
+
# Align input values to expected column order
|
| 139 |
+
input_dict = dict(zip(expected_cols, input_data))
|
| 140 |
+
input_df = pd.DataFrame([input_dict])[expected_cols]
|
| 141 |
+
|
| 142 |
+
# DEBUG VIEW
|
| 143 |
+
st.write("📄 Aligned Input DataFrame:")
|
| 144 |
+
st.dataframe(input_df)
|
| 145 |
+
|
| 146 |
+
# Transform
|
| 147 |
+
x_scaled = scaler.transform(input_df)
|
| 148 |
+
x_pca = pca.transform(x_scaled)
|
| 149 |
+
proba = model.predict_proba(x_pca)[0][1]
|
| 150 |
+
prediction = "Increase" if proba >= 0.62 else "Decrease"
|
| 151 |
+
return prediction, round(proba, 4)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
import gspread
|
| 155 |
+
from oauth2client.service_account import ServiceAccountCredentials
|
| 156 |
+
|
| 157 |
+
def log_prediction(record):
|
| 158 |
+
try:
|
| 159 |
+
scope = ["https://spreadsheets.google.com/feeds",
|
| 160 |
+
"https://www.googleapis.com/auth/drive"]
|
| 161 |
+
|
| 162 |
+
creds = ServiceAccountCredentials.from_json_keyfile_name("creds.json", scope)
|
| 163 |
+
client = gspread.authorize(creds)
|
| 164 |
+
|
| 165 |
+
sheet = client.open("BTC Predictions Log").sheet1 # Must match your actual Google Sheet name
|
| 166 |
+
sheet.append_row(list(record.values()))
|
| 167 |
+
st.success("✅ Logged to Google Sheet successfully.")
|
| 168 |
+
except Exception as e:
|
| 169 |
+
st.warning(f"⚠️ Logging to Google Sheets failed: {e}")
|
| 170 |
+
|
| 171 |
+
# ---------------------------- STREAMLIT UI ----------------------------
|
| 172 |
+
st.set_page_config(page_title="Next Day Bitcoin Price Movement", layout="wide")
|
| 173 |
+
st.title("🔮 Next Day Bitcoin Price Movement Predictor")
|
| 174 |
+
|
| 175 |
+
date = st.date_input("Select a date", datetime.today() - timedelta(days=1))
|
| 176 |
+
month = date.strftime("%Y-%m")
|
| 177 |
+
|
| 178 |
+
if "news_loaded" not in st.session_state:
|
| 179 |
+
st.session_state.news_loaded = False
|
| 180 |
+
|
| 181 |
+
sentiment_features = []
|
| 182 |
+
aggregated_display = {}
|
| 183 |
+
news_by_source = {"CryptoNews": [], "CryptoPotato": []}
|
| 184 |
+
edited_news_by_source = {}
|
| 185 |
+
|
| 186 |
+
# ------------------------------------
|
| 187 |
+
# STEP 1: FETCH NEWS + ENABLE EDITING
|
| 188 |
+
# ------------------------------------
|
| 189 |
+
if not st.session_state.news_loaded:
|
| 190 |
+
if st.button("📥 Fetch News"):
|
| 191 |
+
for src in ["CryptoNews", "CryptoPotato"]:
|
| 192 |
+
try:
|
| 193 |
+
news = fetch_news(src)
|
| 194 |
+
news_by_source[src] = news
|
| 195 |
+
st.session_state[src] = "\n\n".join(news) # store for text_area default
|
| 196 |
+
except Exception as e:
|
| 197 |
+
st.warning(f"⚠️ Could not fetch {src}: {e}")
|
| 198 |
+
st.session_state[src] = ""
|
| 199 |
+
st.session_state.news_loaded = True
|
| 200 |
+
st.rerun()
|
| 201 |
+
|
| 202 |
+
# ------------------------------------
|
| 203 |
+
# STEP 2: SHOW TEXT BOXES + RUN PREDICTION
|
| 204 |
+
# ------------------------------------
|
| 205 |
+
if st.session_state.news_loaded:
|
| 206 |
+
st.subheader("📝 Edit News Articles")
|
| 207 |
+
for src in ["CryptoNews", "CryptoPotato"]:
|
| 208 |
+
default_text = st.session_state.get(src, "")
|
| 209 |
+
user_input = st.text_area(f"{src} Articles (5 max, one per paragraph)", default_text, height=300)
|
| 210 |
+
edited_news_by_source[src] = [para.strip() for para in user_input.split("\n\n") if para.strip()]
|
| 211 |
+
|
| 212 |
+
if st.button("🔮 Make Prediction"):
|
| 213 |
+
for src in ["CryptoNews", "CryptoPotato"]:
|
| 214 |
+
try:
|
| 215 |
+
news_by_source[src] = edited_news_by_source[src]
|
| 216 |
+
scores_df = call_finbert(news_by_source[src])
|
| 217 |
+
st.write(f"📊 FinBERT Scores for {src}:", scores_df)
|
| 218 |
+
|
| 219 |
+
weighted_avg, extreme_count = aggregate_sentiments(scores_df)
|
| 220 |
+
total_articles = len(scores_df)
|
| 221 |
+
|
| 222 |
+
pct_scores = {
|
| 223 |
+
"positive_pct": extreme_count.get("positive", 0) / total_articles,
|
| 224 |
+
"neutral_pct": extreme_count.get("neutral", 0) / total_articles,
|
| 225 |
+
"negative_pct": extreme_count.get("negative", 0) / total_articles
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
sentiment_features.extend([
|
| 229 |
+
weighted_avg["positive"],
|
| 230 |
+
weighted_avg["neutral"],
|
| 231 |
+
weighted_avg["negative"],
|
| 232 |
+
pct_scores["positive_pct"],
|
| 233 |
+
pct_scores["neutral_pct"],
|
| 234 |
+
pct_scores["negative_pct"]
|
| 235 |
+
])
|
| 236 |
+
except Exception as e:
|
| 237 |
+
st.warning(f"⚠️ Failed for {src}: {e}")
|
| 238 |
+
sentiment_features.extend([0.0] * 6)
|
| 239 |
+
news_by_source[src] = []
|
| 240 |
+
|
| 241 |
+
st.markdown("**Aggregated Sentiment**")
|
| 242 |
+
st.write("🔎 News by Source:", news_by_source)
|
| 243 |
+
sentiment_feature_labels = {
|
| 244 |
+
"cryptonews_positive_weighted": sentiment_features[0],
|
| 245 |
+
"cryptonews_neutral_weighted": sentiment_features[1],
|
| 246 |
+
"cryptonews_negative_weighted": sentiment_features[2],
|
| 247 |
+
"cryptonews_positive_pct": sentiment_features[3],
|
| 248 |
+
"cryptonews_neutral_pct": sentiment_features[4],
|
| 249 |
+
"cryptonews_negative_pct": sentiment_features[5],
|
| 250 |
+
"cryptopotato_positive_weighted": sentiment_features[6],
|
| 251 |
+
"cryptopotato_neutral_weighted": sentiment_features[7],
|
| 252 |
+
"cryptopotato_negative_weighted": sentiment_features[8],
|
| 253 |
+
"cryptopotato_positive_pct": sentiment_features[9],
|
| 254 |
+
"cryptopotato_neutral_pct": sentiment_features[10],
|
| 255 |
+
"cryptopotato_negative_pct": sentiment_features[11],
|
| 256 |
+
}
|
| 257 |
+
st.markdown("### 🧠 Sentiment Features by Source")
|
| 258 |
+
st.json(sentiment_feature_labels)
|
| 259 |
+
|
| 260 |
+
# Average across both sources
|
| 261 |
+
if len(sentiment_features) == 12:
|
| 262 |
+
aggregated_sentiments = [
|
| 263 |
+
(sentiment_features[0] + sentiment_features[6]) / 2,
|
| 264 |
+
(sentiment_features[1] + sentiment_features[7]) / 2,
|
| 265 |
+
(sentiment_features[2] + sentiment_features[8]) / 2,
|
| 266 |
+
(sentiment_features[3] + sentiment_features[9]) / 2,
|
| 267 |
+
(sentiment_features[4] + sentiment_features[10]) / 2,
|
| 268 |
+
(sentiment_features[5] + sentiment_features[11]) / 2
|
| 269 |
+
]
|
| 270 |
+
elif len(sentiment_features) == 6:
|
| 271 |
+
aggregated_sentiments = sentiment_features
|
| 272 |
+
else:
|
| 273 |
+
st.warning("⚠️ Sentiment features incomplete. Defaulting to 0s.")
|
| 274 |
+
aggregated_sentiments = [0.0] * 6
|
| 275 |
+
|
| 276 |
+
# Fetch BTC + macro data
|
| 277 |
+
st.subheader("📈 Bitcoin Price Data")
|
| 278 |
+
btc = fetch_yahoo_data(TICKERS["bitcoin"], date)
|
| 279 |
+
st.json(btc)
|
| 280 |
+
|
| 281 |
+
st.subheader("📊 Macroeconomic Indicators")
|
| 282 |
+
macro = {}
|
| 283 |
+
for k, t in TICKERS.items():
|
| 284 |
+
if k != "bitcoin":
|
| 285 |
+
try:
|
| 286 |
+
macro[k] = fetch_yahoo_data(t, date)
|
| 287 |
+
except Exception as e:
|
| 288 |
+
st.warning(f"⚠️ Failed to fetch {k.upper()} data: {e}")
|
| 289 |
+
macro[k] = {"open": 0, "high": 0, "low": 0, "close": 0, "volume": 0, "change_pct": 0}
|
| 290 |
+
st.json(macro)
|
| 291 |
+
|
| 292 |
+
st.subheader("🏩 Fed Indicators")
|
| 293 |
+
fed = {
|
| 294 |
+
"interest_rate": fetch_fred(FRED_CODES["interest_rate"], month),
|
| 295 |
+
"inflation": fetch_fred(FRED_CODES["inflation"], month)
|
| 296 |
+
}
|
| 297 |
+
st.json(fed)
|
| 298 |
+
|
| 299 |
+
# ========== BUILD FINAL INPUT DICT SAFELY ==========
|
| 300 |
+
final_input_dict = {
|
| 301 |
+
"S&P_500_Open": macro["sp500"].get("open", 0),
|
| 302 |
+
"S&P_500_High": macro["sp500"].get("high", 0),
|
| 303 |
+
"S&P_500_Low": macro["sp500"].get("low", 0),
|
| 304 |
+
"S&P_500_Close": macro["sp500"].get("close", 0),
|
| 305 |
+
"S&P_500_Volume": macro["sp500"].get("volume", 0),
|
| 306 |
+
"S&P_500_%_Change": macro["sp500"].get("change_pct", 0),
|
| 307 |
+
|
| 308 |
+
"Gold_Prices_Open": macro["gold"].get("open", 0),
|
| 309 |
+
"Gold_Prices_High": macro["gold"].get("high", 0),
|
| 310 |
+
"Gold_Prices_Low": macro["gold"].get("low", 0),
|
| 311 |
+
"Gold_Prices_Close": macro["gold"].get("close", 0),
|
| 312 |
+
"Gold_Prices_Volume": macro["gold"].get("volume", 0),
|
| 313 |
+
"Gold_Prices_%_Change": macro["gold"].get("change_pct", 0),
|
| 314 |
+
|
| 315 |
+
"US_Dollar_Index_DXY_Open": macro["dxy"].get("open", 0),
|
| 316 |
+
"US_Dollar_Index_DXY_High": macro["dxy"].get("high", 0),
|
| 317 |
+
"US_Dollar_Index_DXY_Low": macro["dxy"].get("low", 0),
|
| 318 |
+
"US_Dollar_Index_DXY_Close": macro["dxy"].get("close", 0),
|
| 319 |
+
"US_Dollar_Index_DXY_%_Change": macro["dxy"].get("change_pct", 0),
|
| 320 |
+
|
| 321 |
+
"Federal_Reserve_Interest_Rates_FEDFUNDS": fed.get("interest_rate", 0),
|
| 322 |
+
"Inflation_CPIAUCNS": fed.get("inflation", 0),
|
| 323 |
+
|
| 324 |
+
"Open": btc.get("open", 0),
|
| 325 |
+
"High": btc.get("high", 0),
|
| 326 |
+
"Low": btc.get("low", 0),
|
| 327 |
+
"Close": btc.get("close", 0),
|
| 328 |
+
"Volume": btc.get("volume", 0),
|
| 329 |
+
"Change %": btc.get("change_pct", 0),
|
| 330 |
+
|
| 331 |
+
"positive_weighted": aggregated_sentiments[0],
|
| 332 |
+
"neutral_weighted": aggregated_sentiments[1],
|
| 333 |
+
"negative_weighted": aggregated_sentiments[2],
|
| 334 |
+
"negative_pct": aggregated_sentiments[5],
|
| 335 |
+
"neutral_pct": aggregated_sentiments[4],
|
| 336 |
+
"positive_pct": aggregated_sentiments[3],
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
# ========== PREPARE & PREDICT ==========
|
| 340 |
+
expected_cols = list(scaler.feature_names_in_)
|
| 341 |
+
final_input = [final_input_dict[col] for col in expected_cols]
|
| 342 |
+
|
| 343 |
+
if any(pd.isna(x) for x in final_input):
|
| 344 |
+
st.error("❌ Missing or invalid input data. Please check news, market, or macro feeds.")
|
| 345 |
+
else:
|
| 346 |
+
# Prepare aligned input
|
| 347 |
+
input_df = pd.DataFrame([final_input_dict])[expected_cols]
|
| 348 |
+
x_scaled = scaler.transform(input_df)
|
| 349 |
+
x_pca = pca.transform(x_scaled)
|
| 350 |
+
|
| 351 |
+
# Model prediction
|
| 352 |
+
proba = model.predict_proba(x_pca)[0][1]
|
| 353 |
+
prediction = "Increase" if proba >= 0.62 else "Decrease"
|
| 354 |
+
|
| 355 |
+
# PCA features table
|
| 356 |
+
pca_df = pd.DataFrame(x_pca, columns=[f"PC{i+1}" for i in range(x_pca.shape[1])])
|
| 357 |
+
st.markdown("### 🧬 PCA-Transformed Features")
|
| 358 |
+
st.dataframe(pca_df.style.format("{:.4f}"))
|
| 359 |
+
|
| 360 |
+
# Prediction display
|
| 361 |
+
st.subheader("🔮 Prediction")
|
| 362 |
+
if prediction == "Decrease":
|
| 363 |
+
st.markdown(
|
| 364 |
+
f"<div style='background-color:#fbeaea;color:#9e1c1c;padding:10px;border-radius:8px;'>"
|
| 365 |
+
f"<b>Next Day BTC Price:</b> {prediction} (Prob: {proba:.2f})</div>",
|
| 366 |
+
unsafe_allow_html=True
|
| 367 |
+
)
|
| 368 |
+
else:
|
| 369 |
+
st.success(f"Next Day BTC Price: **{prediction}** (Prob: {proba:.2f})")
|
| 370 |
+
|
| 371 |
+
# Log prediction
|
| 372 |
+
log = {
|
| 373 |
+
"fetch_date": datetime.today().strftime("%Y-%m-%d"),
|
| 374 |
+
"btc_open": btc["open"],
|
| 375 |
+
"btc_close": btc["close"],
|
| 376 |
+
"sent_pos": aggregated_sentiments[0],
|
| 377 |
+
"sent_neu": aggregated_sentiments[1],
|
| 378 |
+
"sent_neg": aggregated_sentiments[2],
|
| 379 |
+
"sent_pos_pct": aggregated_sentiments[3],
|
| 380 |
+
"sent_neu_pct": aggregated_sentiments[4],
|
| 381 |
+
"sent_neg_pct": aggregated_sentiments[5],
|
| 382 |
+
"macro_gold": macro["gold"]["close"],
|
| 383 |
+
"macro_sp500": macro["sp500"]["close"],
|
| 384 |
+
"macro_dxy": macro["dxy"]["close"],
|
| 385 |
+
"interest_rate": fed["interest_rate"],
|
| 386 |
+
"inflation": fed["inflation"],
|
| 387 |
+
"prediction": prediction,
|
| 388 |
+
"prob": proba,
|
| 389 |
+
"news_cryptonews": " || ".join(news_by_source["CryptoNews"]),
|
| 390 |
+
"news_cryptopotato": " || ".join(news_by_source["CryptoPotato"])
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
log_prediction(log)
|
| 394 |
+
st.success("✅ Logged to predictions_log.csv")
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
|
creds.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "service_account",
|
| 3 |
+
"project_id": "bitcoin-predictor-log",
|
| 4 |
+
"private_key_id": "c66f0bb452bb5ffcaa9b23ac93eb42347000d94f",
|
| 5 |
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCagBsCFFxFDHOJ\nuJGm7iQMU8+j/xnCpvep4/+lMvtCVlZsHD+C8SN23IIVC73oNHSMIOkUT7vjxlRP\nZ9bQSgLZ1KXspfH0PdjYJH1pxJdnElgKIUTK5NTcj33ZAKWa/XpkAsaZMHs7QUuQ\nlkhxsATVNOK7c/QtBMqqcMh14SHDUHBm8zncDoRg8Rmf0uaMEs+zXPPbOevBtEFo\nQH82k5jGSR8oevGfCFdOnXngfyyhX9Unm21C9TqpdL0ROGIj5V8yTMHTBKkM62MQ\nLfeKABufx+uGb4H4Paa2ZlJacFcXY7bsj6OsXxdeulNZLB5AQg6QQm1jtx40t0oK\nsxr2k+ItAgMBAAECggEAELFKGbuq62/otMbK/9LndKiCfOjFNv10sTeXyZi8QmLT\nJAuaRhK5HvC8ojr6SfIO7Ivqut3Rgk7NkaW5tRfl/nSF989HGLks6k9o+Go75HA8\nMF3/UX+PSwQ6190Ex33fARq2q9rr2Y9Ys3a1PYlDwGheHdwtk8aagfc9BVVtYS1u\n+CHntFZb8J6ozTDQREe3qL9v0YzEKaWeReBVE1d+j++EtpeFij9v/sl2d1oQMTLH\nPzrKeMzbMU3OqTWENhAIVjihuqZFZY99dAMdmyh1QtGrUV7Iic8ixM9fFB/aF9EQ\nsu7pmOQu2jPdB6Dkxwwy3S8yimOb2MyGPrBTAHx8wQKBgQDKiZ0ZcZbB7HsGLUAK\n0s7AJ3DoVh2KTxwsmmfoZIJgfu16JlSRWUznQdORJ/kTR4b6E6o36N2GrSjmIC5O\n0UKoaeoGoo3azjf+5C+dJ18nQ3wd18gDvMb2/yehsBPASwVnPie28ZN1664Tx8wP\ni0rJuadlqVzgY2n3d8ueHK8YwQKBgQDDSGcwNzLZMBAIeumA9rzmW+WESPlbgy3D\nYnkLP9jy9I1zazAbGW9S8AU4XdRc0tkHjCdzY7dC9hGUAEyTRS1n4C93U5HpJAzP\nQH/ob1Wsz2DCTR18dRZeTFKECHcHMjWjFJVMHvpjNGuyS4TLLoSJRhE4PnSEZOzJ\n8FVqzIRYbQKBgQDJKlfMPsLzR/OFVgpm9az+s+30Bhx/FEYykwYjjzjjqQ4sJcgX\nObAlfX8qjJ5apM+OsVt+/p6QtoqJz2rPRA9GATS9dFUa/3okg/Y6zDE5aVDsvzbd\nZ3HjP9jYQm/LrXfnbJe7oEPLetPCt86Zncshg3GditNB19wXPHgUSf8rwQKBgQCD\nql6AgMcU7rXwscacQEAO5SjzrywJSoHheZR5RDwnW0G/7yZJLzYC6nfqkEDtsO/J\nifLTdwkJ6dTiP+1hYkQCBIKcZsk7MyY72pYjBmXylQP9HXdjAaLqQ3VjNj3iqTTG\n1sruvvg9SQvP8+D+CUgtMgPMMzfmSBHq+dLtpiUZZQKBgQCYzwgY9k6xD1EEMljJ\n66UAEiQiIJOomatjmxc4MNrK1Zsows6f40qnJ22PhqNuKHqCGyj+qnQP9o1MWJGC\n7McvxUR16Z2oEtgRPUDMXe+I2PBaRL76m1SZ3cYV4Hm5HJXacoJU35bqo5NKQQi7\nlf6mgxpvZipUt/s3tChvhDpeeg==\n-----END PRIVATE KEY-----\n",
|
| 6 |
+
"client_email": "btc-predictor-logger@bitcoin-predictor-log.iam.gserviceaccount.com",
|
| 7 |
+
"client_id": "114024114053631353428",
|
| 8 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 9 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
| 10 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 11 |
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/btc-predictor-logger%40bitcoin-predictor-log.iam.gserviceaccount.com",
|
| 12 |
+
"universe_domain": "googleapis.com"
|
| 13 |
+
}
|
histgb_pca_model_clean.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3465f30c06057d147bba5342686788c87fab93a67be6a158e5e5c93faeb8a1f9
|
| 3 |
+
size 33944
|
pca.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ed7c470f4c88de93c41296fde8af77202271d070be7eab95ae4425dabb7f5ed
|
| 3 |
+
size 4223
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
numpy==1.24.4
|
| 3 |
+
pandas
|
| 4 |
+
scikit-learn
|
| 5 |
+
matplotlib
|
| 6 |
+
seaborn
|
| 7 |
+
joblib
|
| 8 |
+
optuna
|
| 9 |
+
shap
|
| 10 |
+
yfinance
|
| 11 |
+
requests
|
| 12 |
+
gspread
|
| 13 |
+
oauth2client
|
scaler.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3ec8c4153f48ee2b3843dfc80bc967528a93bb152aeafe43fbfd4257fc90f46b
|
| 3 |
+
size 2319
|