Spaces:
Sleeping
Sleeping
Commit
·
aac9e56
1
Parent(s):
fc5b652
Dockerized MLOps pipeline with drift-aware retraining
Browse files- .dockerignore +27 -0
- Dockerfile +16 -0
- dvc.yaml +52 -0
- gnews_data.csv +17 -0
- live.py +122 -213
- news_articles.csv +169 -0
- params.yaml +15 -0
- data/reddit_data.csv → reddit_data.csv +0 -0
- requirements.txt +31 -18
- src/build_features.py +146 -0
- src/drift_detection.py +56 -0
- src/evaluate_models.py +37 -0
- src/ingest_data.py +62 -0
- src/live_service.py +44 -0
- src/retrain_if_drift.py +14 -0
- src/should_retrain.py +27 -0
- src/train_models.py +156 -0
.dockerignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git & DVC
|
| 2 |
+
.git
|
| 3 |
+
.dvc
|
| 4 |
+
.dvc/cache
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
__pycache__
|
| 8 |
+
*.pyc
|
| 9 |
+
*.pyo
|
| 10 |
+
*.pyd
|
| 11 |
+
|
| 12 |
+
# Virtual environments
|
| 13 |
+
venv
|
| 14 |
+
venv-drift
|
| 15 |
+
|
| 16 |
+
# ML artifacts (DO NOT bake into image)
|
| 17 |
+
models
|
| 18 |
+
mlruns
|
| 19 |
+
drift_reports
|
| 20 |
+
|
| 21 |
+
# Data (optional – keep only if you want runtime download)
|
| 22 |
+
data/raw
|
| 23 |
+
data/processed
|
| 24 |
+
|
| 25 |
+
# OS junk
|
| 26 |
+
.DS_Store
|
| 27 |
+
Thumbs.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Install Python dependencies
|
| 9 |
+
COPY requirements.txt .
|
| 10 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
# Copy project files
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
# Run full pipeline
|
| 16 |
+
CMD ["dvc", "repro"]
|
dvc.yaml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
stages:
|
| 2 |
+
ingest:
|
| 3 |
+
cmd: python src/ingest_data.py
|
| 4 |
+
outs:
|
| 5 |
+
- data/raw
|
| 6 |
+
|
| 7 |
+
features:
|
| 8 |
+
cmd: python src/build_features.py
|
| 9 |
+
deps:
|
| 10 |
+
- data/raw
|
| 11 |
+
outs:
|
| 12 |
+
- data/processed/merged_features.csv
|
| 13 |
+
|
| 14 |
+
train:
|
| 15 |
+
cmd: python src/train_models.py
|
| 16 |
+
deps:
|
| 17 |
+
- data/processed/merged_features.csv
|
| 18 |
+
outs:
|
| 19 |
+
- models
|
| 20 |
+
|
| 21 |
+
evaluate:
|
| 22 |
+
cmd: python src/evaluate_models.py
|
| 23 |
+
deps:
|
| 24 |
+
- models
|
| 25 |
+
- data/processed/merged_features.csv
|
| 26 |
+
outs:
|
| 27 |
+
- metrics/evaluation.json
|
| 28 |
+
|
| 29 |
+
drift:
|
| 30 |
+
cmd: python src/drift_detection.py
|
| 31 |
+
deps:
|
| 32 |
+
- src/drift_detection.py
|
| 33 |
+
- data/processed/merged_features.csv
|
| 34 |
+
outs:
|
| 35 |
+
- drift_reports/drift_summary.json
|
| 36 |
+
|
| 37 |
+
retrain_decision:
|
| 38 |
+
cmd: python src/should_retrain.py
|
| 39 |
+
deps:
|
| 40 |
+
- src/should_retrain.py
|
| 41 |
+
- drift_reports/drift_summary.json
|
| 42 |
+
outs:
|
| 43 |
+
- drift_reports/retrain_flag.json
|
| 44 |
+
|
| 45 |
+
retrain:
|
| 46 |
+
cmd: python src/retrain_if_drift.py
|
| 47 |
+
deps:
|
| 48 |
+
- src/retrain_if_drift.py
|
| 49 |
+
- src/train_models.py
|
| 50 |
+
- drift_reports/retrain_flag.json
|
| 51 |
+
|
| 52 |
+
|
gnews_data.csv
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
source,author,title,description,url,publishedAt,content
|
| 2 |
+
The Mercury News,,Netflix blames tax dispute in Brazil for rare quarterly earnings letdown,"Netflix missed the earnings target set by stock market analysts during the video streamer's latest quarter, a letdown that the company blamed on a tax dispute in Brazil.",https://www.mercurynews.com/2025/10/22/netflix-tax-dispute-brazil-quarterly-earnings-letdown/,2025-10-22T16:03:04Z,"By MICHAEL LIEDTKE
|
| 3 |
+
Netflix missed the earnings target set by stock market analysts during the video streamer’s latest quarter, a letdown that the company blamed on a tax dispute in Brazil.
|
| 4 |
+
The results announced Tuesday broke Netflix’s six-quarter str... [4150 chars]"
|
| 5 |
+
Benzinga,,QuantumScape Stock Slides Ahead Of Q3 Earnings: What's Going On?,QuantumScape shares are trading lower Wednesday morning as the market anticipates its third-quarter earnings report after the closing bell.,https://www.benzinga.com/trading-ideas/movers/25/10/48357671/quantumscape-stock-slides-ahead-of-q3-earnings-whats-going-on,2025-10-22T15:34:13Z,"QuantumScape Corp (NYSE:QS) shares are trading lower Wednesday morning as the market anticipates its third-quarter earnings report after the closing bell. This pullback follows a significant rally that saw the stock last week hit a new 52-week high.
|
| 6 |
+
... [2227 chars]"
|
| 7 |
+
Investor's Business Daily,,Home Depot Stock: One Of America's Greatest Opportunities,"In 1981, in the middle of a bear market, Home Depot stock had its initial public offering. Shares quickly grew by more than 1,000%.",https://www.investors.com/how-to-invest/home-depot-hd-stock-americas-greatest-opportunities/,2025-10-22T14:07:36Z,"Information in Investor’s Business Daily is for informational and educational purposes only and should not be construed as an offer, recommendation, solicitation, or rating to buy or sell securities. The information has been obtained from sources we ... [1013 chars]"
|
| 8 |
+
Bloomberg,,S&P 500 Steady as Traders Weigh ‘Split’ Market; Netflix Tumbles,"US stock declines deepened after news that the Trump administration is considering curbs on software exports to China. Big tech led S&P 500 Index losses on a points basis, with Apple Inc. falling 2.4% and Nvidia Corp. shedding 2.1%.",https://www.bloomberg.com/news/articles/2025-10-22/s-p-500-steady-as-traders-weigh-split-market-netflix-tumbles,2025-10-22T13:55:51Z,"US stock declines deepened after newsBloomberg Terminal that the Trump administration is considering curbs on software exports to China. Big tech led S&P 500 Index losses on a points basis, with Apple Inc. falling 2.4% and Nvidia Corp. shedding 2.1% ... [401 chars]"
|
| 9 |
+
CNBC,,Jim Cramer's top 10 things to watch in the stock market Wednesday,Stock futures were muted Tuesday as investors awaiting another batch of corporate earnings.,https://www.cnbc.com/2025/10/22/jim-cramers-top-10-things-to-watch-in-the-stock-market-wednesday.html,2025-10-22T12:57:16Z,"My top 10 things to watch Wednesday, Oct. 22 1. GE Vernova reported an earnings beat this morning and reaffirmed its full-year targets. The Club stock posted excellent organic orders for gas turbines, but miserable orders for wind. Wind is a rounding... [3999 chars]"
|
| 10 |
+
Benzinga,,How To Earn $500 A Month From IBM Stock Ahead Of Q3 Earnings,"IBM (NYSE: IBM) to release Q3 earnings after market close on Wednesday. Analysts expect $2.45/share earnings and $16.1B revenue. IBM offers 2.38% annual dividend yield, investors can earn $500/month with $251,871 investment or $100/month with $50,487 investment.",https://www.benzinga.com/trading-ideas/dividends/25/10/48350802/how-to-earn-500-a-month-from-ibm-stock-ahead-of-q3-earnings-2,2025-10-22T12:40:43Z,"IBM (NYSE:IBM) will release earnings results for the third quarter after the closing bell on Wednesday.
|
| 11 |
+
Analysts expect the company to report quarterly earnings at $2.45 per share, up from $2.30 per share in the year-ago period. The consensus estimat... [1973 chars]"
|
| 12 |
+
Benzinga,,Why Is Wellgistics Health Stock Soaring Wednesday?,DataVault AI and Wellgistics Health are collaborating to bring blockchain technology to the US prescription drug market.,https://www.benzinga.com/trading-ideas/movers/25/10/48350651/wellgistics-and-datavault-team-up-to-transform-prescription-tracking,2025-10-22T12:30:33Z,"DataVault AI, Inc. (NASDAQ:DVLT) and Wellgistics Health, Inc. (NASDAQ:WGRX) are collaborating to bring blockchain technology to the U.S. prescription drug market.
|
| 13 |
+
Through a non-binding letter of intent, the companies plan to integrate DataVault’s blo... [2109 chars]"
|
| 14 |
+
Investopedia,,5 Things to Know Before the Stock Market Opens,Stock futures are little changed as investors digest a weaker-than-expected earnings report from Netflix and look ahead to Tesla’s quarterly results after the closing bell. Here's what you need to know today.,https://www.investopedia.com/5-things-to-know-before-the-stock-market-opens-october-22-2025-11834499,2025-10-22T12:22:49Z,Stock futures are holding steady this morning as quarterly earnings reports continue to roll in and the government shutdown begins its fourth week; gold futures are losing ground for the second straight day after hitting a series of record highs; Tes... [3645 chars]
|
| 15 |
+
The Street,,Stock Market Today: Stocks Decline As Earnings Season Picks Up Pace,This live blog is refreshed periodically throughout the day with the latest updates from the market. To find the latest Stock Market,https://www.thestreet.com/markets/stock-market-today-stocks-decline-as-earnings-season-picks-up-pace,2025-10-22T12:12:33Z,"This live blog is refreshed periodically throughout the day with the latest updates from the market. To find the latest Stock Market Today threads, click here.
|
| 16 |
+
Happy Wednesday. This is TheStreet’s Stock Market Today for Oct. 22, 2025. You can follow ... [2530 chars]"
|
| 17 |
+
Investor's Business Daily,,Stock Market Today: Dow Falls As Netflix Dives On Q3 Miss; Tesla Earnings Next (Live),Stock Market Today: The Dow Jones index fell Wednesday as Netflix dived on an earnings miss. Tesla earnings are due after the close.,https://www.investors.com/market-trend/stock-market-today/dow-jones-sp500-nasdaq-netflix-nflx-stock-tesla-earnings-tsla-stock/,2025-10-22T12:10:02Z,"Information in Investor’s Business Daily is for informational and educational purposes only and should not be construed as an offer, recommendation, solicitation, or rating to buy or sell securities. The information has been obtained from sources we ... [1013 chars]"
|
live.py
CHANGED
|
@@ -5,6 +5,7 @@ import numpy as np
|
|
| 5 |
import pandas as pd
|
| 6 |
import joblib
|
| 7 |
import warnings
|
|
|
|
| 8 |
from datetime import datetime
|
| 9 |
|
| 10 |
import mlflow
|
|
@@ -22,7 +23,12 @@ from sklearn.ensemble import RandomForestRegressor
|
|
| 22 |
import yfinance as yf
|
| 23 |
from gnewsclient import gnewsclient
|
| 24 |
|
| 25 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
warnings.filterwarnings("ignore", category=FutureWarning)
|
| 27 |
warnings.filterwarnings("ignore", category=UserWarning, module="fuzzywuzzy")
|
| 28 |
|
|
@@ -35,36 +41,50 @@ os.makedirs("./data", exist_ok=True)
|
|
| 35 |
FETCH_INTERVAL_SECONDS = 60
|
| 36 |
SEQ_LENGTH = 10
|
| 37 |
|
| 38 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
def load_stock_data(path="data/stock_prices.csv"):
|
| 40 |
if not os.path.exists(path):
|
| 41 |
print(f"{path} not found → empty DF")
|
| 42 |
return pd.DataFrame(columns=["Date", "Ticker", "Close", "High", "Low", "Open", "Volume", "Return"])
|
| 43 |
|
| 44 |
df = pd.read_csv(path, low_memory=False)
|
| 45 |
-
|
| 46 |
-
# Use 'date' if exists, else 'Date'
|
| 47 |
date_col = 'date' if 'date' in df.columns else 'Date'
|
| 48 |
df['Date'] = pd.to_datetime(df[date_col], errors='coerce')
|
| 49 |
-
|
| 50 |
-
# DROP bogus 1970 dates
|
| 51 |
df = df[df['Date'].dt.year >= 2000].copy()
|
| 52 |
-
|
| 53 |
-
# FORCE TZ: America/New_York
|
| 54 |
df['Date'] = df['Date'].dt.tz_localize('UTC').dt.tz_convert('America/New_York')
|
| 55 |
-
|
| 56 |
-
# Clean numeric
|
| 57 |
for col in ['Close', 'High', 'Low', 'Open', 'Volume', 'Return']:
|
| 58 |
if col in df.columns:
|
| 59 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 60 |
-
|
| 61 |
df = df[['Date', 'Ticker', 'Close', 'High', 'Low', 'Open', 'Volume', 'Return']]
|
| 62 |
df = df.sort_values(['Ticker', 'Date']).reset_index(drop=True)
|
| 63 |
-
|
| 64 |
print(f"Loaded {len(df)} valid rows (post-2000, tz-aware)")
|
| 65 |
return df
|
| 66 |
|
| 67 |
-
# 2. LOAD TEXT DATA
|
| 68 |
def load_text_data(paths=["data/news_articles.csv", "data/gnews_data.csv"]):
|
| 69 |
dfs = []
|
| 70 |
for p, src in zip(paths, ["news", "gnews"]):
|
|
@@ -86,7 +106,7 @@ def load_text_data(paths=["data/news_articles.csv", "data/gnews_data.csv"]):
|
|
| 86 |
txt = txt.dropna(subset=["date"])
|
| 87 |
return txt
|
| 88 |
|
| 89 |
-
# 3. SENTIMENT
|
| 90 |
POS_WORDS = ["good", "buy", "up", "rise", "gain", "positive", "bull", "strong", "profit", "growth", "high", "best", "win", "success", "pump", "moon", "rocket"]
|
| 91 |
NEG_WORDS = ["bad", "sell", "down", "fall", "loss", "negative", "bear", "weak", "decline", "low", "worst", "fail", "crash", "risk", "dump", "scam"]
|
| 92 |
|
|
@@ -95,20 +115,16 @@ def simple_sentiment(text):
|
|
| 95 |
pos_count = sum(1 for word in words if word in POS_WORDS)
|
| 96 |
neg_count = sum(1 for word in words if word in NEG_WORDS)
|
| 97 |
total = pos_count + neg_count
|
| 98 |
-
if total
|
| 99 |
-
return 0
|
| 100 |
-
return (pos_count - neg_count) / total
|
| 101 |
|
| 102 |
-
# 4. LIVE FETCH STOCKS
|
| 103 |
def fetch_live_stocks(tickers=["AAPL", "GOOGL", "TSLA"], period="1d", interval="1m"):
|
| 104 |
rows = []
|
| 105 |
for t in tickers:
|
| 106 |
try:
|
| 107 |
df = yf.download(t, period=period, interval=interval, progress=False, auto_adjust=False, threads=False, prepost=True)
|
| 108 |
-
if df.empty:
|
| 109 |
-
|
| 110 |
-
if isinstance(df.columns, pd.MultiIndex):
|
| 111 |
-
df.columns = [col[0] for col in df.columns]
|
| 112 |
df.columns = [str(col).lower().strip() for col in df.columns]
|
| 113 |
df = df.rename(columns={'open':'Open','high':'High','low':'Low','close':'Close','volume':'Volume'})
|
| 114 |
df = df[['Open','High','Low','Close','Volume']].reset_index()
|
|
@@ -116,64 +132,47 @@ def fetch_live_stocks(tickers=["AAPL", "GOOGL", "TSLA"], period="1d", interval="
|
|
| 116 |
df['Date'] = pd.to_datetime(df['Date'], utc=True).dt.tz_convert('America/New_York')
|
| 117 |
df['Ticker'] = t
|
| 118 |
rows.append(df[['Date','Ticker','Open','High','Low','Close','Volume']])
|
| 119 |
-
except Exception as e:
|
| 120 |
-
|
| 121 |
-
if not rows:
|
| 122 |
-
return pd.DataFrame()
|
| 123 |
new = pd.concat(rows, ignore_index=True)
|
| 124 |
new = new.sort_values(['Ticker','Date']).reset_index(drop=True)
|
| 125 |
new['Return'] = new.groupby('Ticker')['Close'].pct_change().fillna(0)
|
| 126 |
print(f"Fetched {len(new)} live rows (latest: {new['Date'].max()})")
|
| 127 |
return new
|
| 128 |
|
| 129 |
-
# 5. LIVE FETCH NEWS
|
| 130 |
def fetch_live_news(max_results=15):
|
| 131 |
try:
|
| 132 |
client = gnewsclient.NewsClient(language="en", location="us", topic="Business", max_results=max_results)
|
| 133 |
items = client.get_news()
|
| 134 |
-
if not items:
|
| 135 |
-
return pd.DataFrame()
|
| 136 |
df = pd.DataFrame(items)
|
| 137 |
print(f"Got {len(df)} news items")
|
| 138 |
-
df['text'] = ''
|
| 139 |
-
for
|
| 140 |
-
|
| 141 |
-
if 'title' in row and pd.notna(row['title']):
|
| 142 |
-
text_parts.append(str(row['title']))
|
| 143 |
-
if 'description' in row and pd.notna(row['description']):
|
| 144 |
-
text_parts.append(str(row['description']))
|
| 145 |
-
elif 'content' in row and pd.notna(row['content']):
|
| 146 |
-
text_parts.append(str(row['content'])[:200])
|
| 147 |
-
df.at[idx, 'text'] = ' '.join(text_parts)
|
| 148 |
-
date_col = next((c for c in ['published', 'pubDate', 'publishedAt', 'date'] if c in df.columns), None)
|
| 149 |
-
if not date_col:
|
| 150 |
-
print("No date column found in news")
|
| 151 |
-
return pd.DataFrame()
|
| 152 |
df['publishedAt'] = pd.to_datetime(df[date_col], errors='coerce')
|
| 153 |
df = df.dropna(subset=['publishedAt'])
|
| 154 |
df['source'] = 'gnews'
|
| 155 |
df['date'] = df['publishedAt'].dt.date
|
| 156 |
-
return df[['text',
|
| 157 |
except Exception as e:
|
| 158 |
print(f"News error: {e}")
|
| 159 |
return pd.DataFrame()
|
| 160 |
|
| 161 |
-
# 6. STREAM LIVE
|
| 162 |
def stream_live(df_prices: pd.DataFrame, df_text: pd.DataFrame, persist=False):
|
| 163 |
cycle = 0
|
| 164 |
while True:
|
| 165 |
cycle += 1
|
| 166 |
print(f"\n[{datetime.now():%Y-%m-%d %H:%M:%S}] Cycle {cycle}")
|
| 167 |
|
|
|
|
| 168 |
new_stk = fetch_live_stocks()
|
| 169 |
if not new_stk.empty:
|
| 170 |
if not df_prices.empty:
|
| 171 |
latest_old = df_prices['Date'].max()
|
| 172 |
-
# Ensure both are tz-aware
|
| 173 |
-
new_stk['Date'] = pd.to_datetime(new_stk['Date'])
|
| 174 |
-
latest_old = pd.to_datetime(latest_old)
|
| 175 |
new_stk = new_stk[new_stk['Date'] > latest_old]
|
| 176 |
-
|
| 177 |
if not new_stk.empty:
|
| 178 |
old_count = len(df_prices)
|
| 179 |
df_prices = pd.concat([df_prices, new_stk])
|
|
@@ -181,17 +180,20 @@ def stream_live(df_prices: pd.DataFrame, df_text: pd.DataFrame, persist=False):
|
|
| 181 |
print(f"Stocks: {old_count} → {len(df_prices)} (+{len(new_stk)} new)")
|
| 182 |
if persist:
|
| 183 |
df_prices.to_csv("data/stock_prices.csv", index=False)
|
| 184 |
-
|
| 185 |
-
print("No newer stock data")
|
| 186 |
|
| 187 |
-
# === NEWS
|
| 188 |
new_news = fetch_live_news()
|
| 189 |
if not new_news.empty:
|
| 190 |
new_news["sentiment"] = new_news["text"].apply(simple_sentiment)
|
|
|
|
| 191 |
df_text = pd.concat([df_text, new_news]).drop_duplicates(subset=["publishedAt"]).sort_values("publishedAt").reset_index(drop=True)
|
|
|
|
| 192 |
if persist:
|
| 193 |
df_text.to_csv("data/live_news_data.csv", index=False)
|
|
|
|
| 194 |
|
|
|
|
| 195 |
if len(df_prices) > 0:
|
| 196 |
df_prices["date"] = df_prices["Date"].dt.date
|
| 197 |
daily_tot = df_text.groupby("date")["sentiment"].mean().reset_index() if len(df_text)>0 else pd.DataFrame()
|
|
@@ -203,39 +205,28 @@ def stream_live(df_prices: pd.DataFrame, df_text: pd.DataFrame, persist=False):
|
|
| 203 |
merged["sentiment_lag1"] = merged.groupby("Ticker")["sentiment"].shift(1).bfill().fillna(0)
|
| 204 |
|
| 205 |
for t in ["AAPL", "GOOGL", "TSLA"]:
|
| 206 |
-
if t not in model_info:
|
| 207 |
-
continue
|
| 208 |
latest = merged[merged["Ticker"] == t].sort_values("Date").iloc[-1]
|
| 209 |
-
point = {
|
| 210 |
-
"return_lag1": latest["Return"],
|
| 211 |
-
"volume_lag1": latest["Volume"],
|
| 212 |
-
"sentiment_lag1": latest["sentiment"]
|
| 213 |
-
}
|
| 214 |
live_predict(t, point, model_info[t])
|
| 215 |
else:
|
| 216 |
-
print("No data")
|
| 217 |
|
| 218 |
time.sleep(FETCH_INTERVAL_SECONDS)
|
| 219 |
|
| 220 |
-
# 7. TORCH HELPERS
|
| 221 |
class TSDataset(Dataset):
|
| 222 |
-
def __init__(self, X, y):
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
def __len__(self):
|
| 226 |
-
return len(self.X)
|
| 227 |
-
def __getitem__(self, idx):
|
| 228 |
-
return self.X[idx], self.y[idx]
|
| 229 |
|
| 230 |
def train_torch(model, loader, epochs=50):
|
| 231 |
-
crit = nn.MSELoss()
|
| 232 |
-
|
| 233 |
-
for epoch in range(epochs):
|
| 234 |
model.train()
|
| 235 |
-
for
|
| 236 |
opt.zero_grad()
|
| 237 |
-
|
| 238 |
-
loss = crit(outputs, batch_y.unsqueeze(1))
|
| 239 |
loss.backward()
|
| 240 |
opt.step()
|
| 241 |
|
|
@@ -243,177 +234,95 @@ def predict_torch(model, loader):
|
|
| 243 |
model.eval()
|
| 244 |
preds = []
|
| 245 |
with torch.no_grad():
|
| 246 |
-
for
|
| 247 |
-
|
| 248 |
-
preds.extend(outputs.squeeze().numpy())
|
| 249 |
return np.array(preds)
|
| 250 |
|
| 251 |
-
# 8. MODELS
|
| 252 |
class MLPModel(nn.Module):
|
| 253 |
-
def __init__(self, input_size):
|
| 254 |
-
|
| 255 |
-
self.fc1 = nn.Linear(input_size, 50)
|
| 256 |
-
self.fc2 = nn.Linear(50, 25)
|
| 257 |
-
self.fc3 = nn.Linear(25, 1)
|
| 258 |
-
|
| 259 |
-
def forward(self, x):
|
| 260 |
-
x = torch.relu(self.fc1(x))
|
| 261 |
-
x = torch.relu(self.fc2(x))
|
| 262 |
-
x = self.fc3(x)
|
| 263 |
-
return x
|
| 264 |
|
| 265 |
class LSTMModel(nn.Module):
|
| 266 |
-
def __init__(self, input_size, hidden_size, num_layers):
|
| 267 |
super().__init__()
|
| 268 |
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
|
| 269 |
self.fc = nn.Linear(hidden_size, 1)
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
-
|
| 272 |
-
out, _ = self.lstm(x)
|
| 273 |
-
out = self.fc(out[:, -1, :])
|
| 274 |
-
return out
|
| 275 |
-
|
| 276 |
-
def create_sequences(data_X, data_y, seq_length):
|
| 277 |
-
xs, ys = [], []
|
| 278 |
-
for i in range(len(data_X) - seq_length):
|
| 279 |
-
x = data_X[i:i+seq_length]
|
| 280 |
-
y = data_y[i+seq_length]
|
| 281 |
-
xs.append(x)
|
| 282 |
-
ys.append(y)
|
| 283 |
-
return np.array(xs), np.array(ys)
|
| 284 |
-
|
| 285 |
-
# 9. TRAIN MODELS FOR TICKER
|
| 286 |
def run_models_for_ticker(ticker, df_merged, seq_length=10):
|
| 287 |
with mlflow.start_run(run_name=f"{ticker}_models"):
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
print(f"{ticker} - RandomForest MSE: {mse_rf:.6f} saved to {rf_path}")
|
| 323 |
-
|
| 324 |
-
input_size = X_t.shape[1]
|
| 325 |
-
mlp = MLPModel(input_size)
|
| 326 |
-
train_ds = TSDataset(X_train_t, y_train_t)
|
| 327 |
-
train_loader_t = DataLoader(train_ds, batch_size=32, shuffle=False)
|
| 328 |
-
train_torch(mlp, train_loader_t)
|
| 329 |
-
test_ds = TSDataset(X_test_t, y_test_t)
|
| 330 |
-
test_loader_t = DataLoader(test_ds, batch_size=32, shuffle=False)
|
| 331 |
-
y_mlp_scaled = predict_torch(mlp, test_loader_t)
|
| 332 |
-
mse_mlp = np.mean((y_test_t - y_mlp_scaled)**2)
|
| 333 |
-
mlp_path = f'saved_models/{ticker}_mlp.pth'
|
| 334 |
-
torch.save(mlp.state_dict(), mlp_path)
|
| 335 |
-
with mlflow.start_run(run_name=f"{ticker}_MLP", nested=True):
|
| 336 |
-
mlflow.log_metric("mse", mse_mlp)
|
| 337 |
-
mlflow.pytorch.log_model(mlp, artifact_path=f"{ticker}_mlp")
|
| 338 |
-
print(f"{ticker} - MLP MSE: {mse_mlp:.6f} saved to {mlp_path}")
|
| 339 |
-
|
| 340 |
-
X_seq_t, y_seq_t = create_sequences(Xs, ys, seq_length)
|
| 341 |
-
if len(X_seq_t) > 0:
|
| 342 |
-
train_size_seq_t = int(len(X_seq_t) * 0.8)
|
| 343 |
-
X_train_seq_t, X_test_seq_t = X_seq_t[:train_size_seq_t], X_seq_t[train_size_seq_t:]
|
| 344 |
-
y_train_seq_t, y_test_seq_t = y_seq_t[:train_size_seq_t], y_seq_t[train_size_seq_t:]
|
| 345 |
-
train_seq_ds = TSDataset(X_train_seq_t, y_train_seq_t)
|
| 346 |
-
train_seq_loader = DataLoader(train_seq_ds, batch_size=32, shuffle=False)
|
| 347 |
-
hidden_size = 50
|
| 348 |
-
num_layers = 2
|
| 349 |
-
lstm = LSTMModel(input_size, hidden_size, num_layers)
|
| 350 |
-
train_torch(lstm, train_seq_loader)
|
| 351 |
-
test_seq_ds = TSDataset(X_test_seq_t, y_test_seq_t)
|
| 352 |
-
test_seq_loader = DataLoader(test_seq_ds, batch_size=32, shuffle=False)
|
| 353 |
-
y_lstm_scaled = predict_torch(lstm, test_seq_loader)
|
| 354 |
-
mse_lstm = np.mean((y_test_seq_t - y_lstm_scaled)**2)
|
| 355 |
-
lstm_path = f'saved_models/{ticker}_lstm.pth'
|
| 356 |
-
torch.save(lstm.state_dict(), lstm_path)
|
| 357 |
-
with mlflow.start_run(run_name=f"{ticker}_LSTM", nested=True):
|
| 358 |
-
mlflow.log_metric("mse", mse_lstm)
|
| 359 |
-
mlflow.pytorch.log_model(lstm, artifact_path=f"{ticker}_lstm")
|
| 360 |
-
print(f"{ticker} - LSTM MSE: {mse_lstm:.6f} saved to {lstm_path}")
|
| 361 |
-
else:
|
| 362 |
-
lstm_path = None
|
| 363 |
-
return {
|
| 364 |
-
"scaler_X": scaler_X_t, "scaler_y": scaler_y_t,
|
| 365 |
-
"rf_path": rf_path, "mlp_path": mlp_path, "lstm_path": lstm_path,
|
| 366 |
-
"input_size": input_size
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
# 10. LIVE PREDICT
|
| 370 |
def live_predict(ticker, point, info):
|
| 371 |
Xnew = np.array([[point["return_lag1"], point["volume_lag1"], point["sentiment_lag1"]]])
|
| 372 |
Xs = info["scaler_X"].transform(Xnew)
|
| 373 |
rf = joblib.load(info["rf_path"])
|
| 374 |
pred_rf = info["scaler_y"].inverse_transform(rf.predict(Xs).reshape(-1,1)).flatten()[0]
|
| 375 |
-
mlp = MLPModel(info["input_size"])
|
| 376 |
-
mlp.
|
| 377 |
-
mlp.eval()
|
| 378 |
-
pred_mlp = info["scaler_y"].inverse_transform(mlp(torch.tensor(Xs, dtype=torch.float32)).squeeze().detach().numpy().reshape(-1,1)).flatten()[0]
|
| 379 |
pred_lstm = np.nan
|
| 380 |
if info["lstm_path"]:
|
| 381 |
-
lstm = LSTMModel(info["input_size"]
|
| 382 |
-
|
| 383 |
-
lstm.
|
| 384 |
-
X_seq_new = np.repeat(Xs, SEQ_LENGTH, axis=0).reshape(1, SEQ_LENGTH, -1)
|
| 385 |
-
pred_lstm = info["scaler_y"].inverse_transform(lstm(torch.tensor(X_seq_new, dtype=torch.float32)).squeeze().detach().numpy().reshape(-1,1)).flatten()[0]
|
| 386 |
print(f"{ticker} - RF: {pred_rf:.6f}, MLP: {pred_mlp:.6f}, LSTM: {pred_lstm:.6f}")
|
| 387 |
-
return pred_rf, pred_mlp, pred_lstm
|
| 388 |
|
| 389 |
-
# MAIN
|
| 390 |
if __name__ == "__main__":
|
|
|
|
|
|
|
| 391 |
df_prices = load_stock_data()
|
| 392 |
df_text = load_text_data()
|
| 393 |
df_text['sentiment'] = df_text['text'].apply(simple_sentiment)
|
| 394 |
df_prices['date'] = df_prices['Date'].dt.date
|
| 395 |
-
|
| 396 |
-
daily_sent_total = daily_sent.groupby('date')['sentiment'].mean().reset_index()
|
| 397 |
daily_sent_total['date'] = pd.to_datetime(daily_sent_total['date']).dt.date
|
| 398 |
df_merged = df_prices.merge(daily_sent_total, on='date', how='left')
|
| 399 |
df_merged['sentiment'] = df_merged['sentiment'].ffill().fillna(0)
|
| 400 |
df_merged = df_merged.sort_values(['Ticker', 'Date']).reset_index(drop=True)
|
| 401 |
df_merged['sentiment_lag1'] = df_merged.groupby('Ticker')['sentiment'].shift(1).bfill().fillna(0)
|
|
|
|
| 402 |
model_info = {}
|
| 403 |
for t in ['AAPL', 'GOOGL', 'TSLA']:
|
| 404 |
print(f"\n=== TRAINING {t} ===")
|
| 405 |
res = run_models_for_ticker(t, df_merged)
|
| 406 |
-
if res
|
| 407 |
-
|
| 408 |
print("\n=== LIVE STREAM STARTED ===")
|
| 409 |
-
|
| 410 |
-
for updated_merged in streamer:
|
| 411 |
-
for t in ['AAPL', 'GOOGL', 'TSLA']:
|
| 412 |
-
if t in model_info:
|
| 413 |
-
latest = updated_merged[updated_merged['Ticker'] == t].sort_values('Date').iloc[-1]
|
| 414 |
-
point = {
|
| 415 |
-
'return_lag1': latest['Return'],
|
| 416 |
-
'volume_lag1': latest['Volume'],
|
| 417 |
-
'sentiment_lag1': latest['sentiment']
|
| 418 |
-
}
|
| 419 |
-
live_predict(t, point, model_info[t])
|
|
|
|
| 5 |
import pandas as pd
|
| 6 |
import joblib
|
| 7 |
import warnings
|
| 8 |
+
import subprocess
|
| 9 |
from datetime import datetime
|
| 10 |
|
| 11 |
import mlflow
|
|
|
|
| 23 |
import yfinance as yf
|
| 24 |
from gnewsclient import gnewsclient
|
| 25 |
|
| 26 |
+
# === DVC CONFIG ===
|
| 27 |
+
DVC_REMOTE = "origin" # or your remote name
|
| 28 |
+
PUSH_AFTER_CYCLE = True # Set False to disable auto-push
|
| 29 |
+
PULL_AT_START = True
|
| 30 |
+
|
| 31 |
+
# === SETTINGS ===
|
| 32 |
warnings.filterwarnings("ignore", category=FutureWarning)
|
| 33 |
warnings.filterwarnings("ignore", category=UserWarning, module="fuzzywuzzy")
|
| 34 |
|
|
|
|
| 41 |
FETCH_INTERVAL_SECONDS = 60
|
| 42 |
SEQ_LENGTH = 10
|
| 43 |
|
| 44 |
+
# === DVC HELPERS ===
|
| 45 |
+
def dvc_pull():
|
| 46 |
+
print("Pulling latest data and models from DVC remote...")
|
| 47 |
+
try:
|
| 48 |
+
subprocess.run(["dvc", "pull", "-r", DVC_REMOTE], check=True)
|
| 49 |
+
print("DVC pull completed.")
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"DVC pull failed: {e}")
|
| 52 |
+
|
| 53 |
+
def dvc_add_and_push(path):
|
| 54 |
+
if not PUSH_AFTER_CYCLE:
|
| 55 |
+
return
|
| 56 |
+
try:
|
| 57 |
+
print(f"Adding {path} to DVC...")
|
| 58 |
+
subprocess.run(["dvc", "add", path], check=True)
|
| 59 |
+
subprocess.run(["git", "add", f"{path}.dvc"], check=True)
|
| 60 |
+
subprocess.run(["git", "commit", "-m", f"Update {path}"], check=True)
|
| 61 |
+
subprocess.run(["dvc", "push", "-r", DVC_REMOTE], check=True)
|
| 62 |
+
print(f"Pushed {path}")
|
| 63 |
+
except Exception as e:
|
| 64 |
+
print(f"DVC push failed for {path}: {e}")
|
| 65 |
+
|
| 66 |
+
# === 1. LOAD STOCK DATA (TZ-AWARE, DVC) ===
|
| 67 |
def load_stock_data(path="data/stock_prices.csv"):
|
| 68 |
if not os.path.exists(path):
|
| 69 |
print(f"{path} not found → empty DF")
|
| 70 |
return pd.DataFrame(columns=["Date", "Ticker", "Close", "High", "Low", "Open", "Volume", "Return"])
|
| 71 |
|
| 72 |
df = pd.read_csv(path, low_memory=False)
|
|
|
|
|
|
|
| 73 |
date_col = 'date' if 'date' in df.columns else 'Date'
|
| 74 |
df['Date'] = pd.to_datetime(df[date_col], errors='coerce')
|
|
|
|
|
|
|
| 75 |
df = df[df['Date'].dt.year >= 2000].copy()
|
|
|
|
|
|
|
| 76 |
df['Date'] = df['Date'].dt.tz_localize('UTC').dt.tz_convert('America/New_York')
|
| 77 |
+
|
|
|
|
| 78 |
for col in ['Close', 'High', 'Low', 'Open', 'Volume', 'Return']:
|
| 79 |
if col in df.columns:
|
| 80 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 81 |
+
|
| 82 |
df = df[['Date', 'Ticker', 'Close', 'High', 'Low', 'Open', 'Volume', 'Return']]
|
| 83 |
df = df.sort_values(['Ticker', 'Date']).reset_index(drop=True)
|
|
|
|
| 84 |
print(f"Loaded {len(df)} valid rows (post-2000, tz-aware)")
|
| 85 |
return df
|
| 86 |
|
| 87 |
+
# === 2. LOAD TEXT DATA ===
|
| 88 |
def load_text_data(paths=["data/news_articles.csv", "data/gnews_data.csv"]):
|
| 89 |
dfs = []
|
| 90 |
for p, src in zip(paths, ["news", "gnews"]):
|
|
|
|
| 106 |
txt = txt.dropna(subset=["date"])
|
| 107 |
return txt
|
| 108 |
|
| 109 |
+
# === 3. SENTIMENT ===
|
| 110 |
POS_WORDS = ["good", "buy", "up", "rise", "gain", "positive", "bull", "strong", "profit", "growth", "high", "best", "win", "success", "pump", "moon", "rocket"]
|
| 111 |
NEG_WORDS = ["bad", "sell", "down", "fall", "loss", "negative", "bear", "weak", "decline", "low", "worst", "fail", "crash", "risk", "dump", "scam"]
|
| 112 |
|
|
|
|
| 115 |
pos_count = sum(1 for word in words if word in POS_WORDS)
|
| 116 |
neg_count = sum(1 for word in words if word in NEG_WORDS)
|
| 117 |
total = pos_count + neg_count
|
| 118 |
+
return (pos_count - neg_count) / total if total else 0.0
|
|
|
|
|
|
|
| 119 |
|
| 120 |
+
# === 4. LIVE FETCH STOCKS ===
|
| 121 |
def fetch_live_stocks(tickers=["AAPL", "GOOGL", "TSLA"], period="1d", interval="1m"):
|
| 122 |
rows = []
|
| 123 |
for t in tickers:
|
| 124 |
try:
|
| 125 |
df = yf.download(t, period=period, interval=interval, progress=False, auto_adjust=False, threads=False, prepost=True)
|
| 126 |
+
if df.empty: continue
|
| 127 |
+
if isinstance(df.columns, pd.MultiIndex): df.columns = [col[0] for col in df.columns]
|
|
|
|
|
|
|
| 128 |
df.columns = [str(col).lower().strip() for col in df.columns]
|
| 129 |
df = df.rename(columns={'open':'Open','high':'High','low':'Low','close':'Close','volume':'Volume'})
|
| 130 |
df = df[['Open','High','Low','Close','Volume']].reset_index()
|
|
|
|
| 132 |
df['Date'] = pd.to_datetime(df['Date'], utc=True).dt.tz_convert('America/New_York')
|
| 133 |
df['Ticker'] = t
|
| 134 |
rows.append(df[['Date','Ticker','Open','High','Low','Close','Volume']])
|
| 135 |
+
except Exception as e: print(f"yfinance {t}: {e}")
|
| 136 |
+
if not rows: return pd.DataFrame()
|
|
|
|
|
|
|
| 137 |
new = pd.concat(rows, ignore_index=True)
|
| 138 |
new = new.sort_values(['Ticker','Date']).reset_index(drop=True)
|
| 139 |
new['Return'] = new.groupby('Ticker')['Close'].pct_change().fillna(0)
|
| 140 |
print(f"Fetched {len(new)} live rows (latest: {new['Date'].max()})")
|
| 141 |
return new
|
| 142 |
|
| 143 |
+
# === 5. LIVE FETCH NEWS ===
|
| 144 |
def fetch_live_news(max_results=15):
|
| 145 |
try:
|
| 146 |
client = gnewsclient.NewsClient(language="en", location="us", topic="Business", max_results=max_results)
|
| 147 |
items = client.get_news()
|
| 148 |
+
if not items: return pd.DataFrame()
|
|
|
|
| 149 |
df = pd.DataFrame(items)
|
| 150 |
print(f"Got {len(df)} news items")
|
| 151 |
+
df['text'] = df['title'].fillna('') + " " + df.get('description', '').fillna('')
|
| 152 |
+
date_col = next((c for c in ['published','pubDate','publishedAt','date'] if c in df.columns), None)
|
| 153 |
+
if not date_col: return pd.DataFrame()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
df['publishedAt'] = pd.to_datetime(df[date_col], errors='coerce')
|
| 155 |
df = df.dropna(subset=['publishedAt'])
|
| 156 |
df['source'] = 'gnews'
|
| 157 |
df['date'] = df['publishedAt'].dt.date
|
| 158 |
+
return df[['text','publishedAt','source','date']]
|
| 159 |
except Exception as e:
|
| 160 |
print(f"News error: {e}")
|
| 161 |
return pd.DataFrame()
|
| 162 |
|
| 163 |
+
# === 6. STREAM LIVE ===
|
| 164 |
def stream_live(df_prices: pd.DataFrame, df_text: pd.DataFrame, persist=False):
|
| 165 |
cycle = 0
|
| 166 |
while True:
|
| 167 |
cycle += 1
|
| 168 |
print(f"\n[{datetime.now():%Y-%m-%d %H:%M:%S}] Cycle {cycle}")
|
| 169 |
|
| 170 |
+
# === FETCH STOCKS ===
|
| 171 |
new_stk = fetch_live_stocks()
|
| 172 |
if not new_stk.empty:
|
| 173 |
if not df_prices.empty:
|
| 174 |
latest_old = df_prices['Date'].max()
|
|
|
|
|
|
|
|
|
|
| 175 |
new_stk = new_stk[new_stk['Date'] > latest_old]
|
|
|
|
| 176 |
if not new_stk.empty:
|
| 177 |
old_count = len(df_prices)
|
| 178 |
df_prices = pd.concat([df_prices, new_stk])
|
|
|
|
| 180 |
print(f"Stocks: {old_count} → {len(df_prices)} (+{len(new_stk)} new)")
|
| 181 |
if persist:
|
| 182 |
df_prices.to_csv("data/stock_prices.csv", index=False)
|
| 183 |
+
dvc_add_and_push("data/stock_prices.csv")
|
|
|
|
| 184 |
|
| 185 |
+
# === FETCH NEWS ===
|
| 186 |
new_news = fetch_live_news()
|
| 187 |
if not new_news.empty:
|
| 188 |
new_news["sentiment"] = new_news["text"].apply(simple_sentiment)
|
| 189 |
+
old_txt = len(df_text)
|
| 190 |
df_text = pd.concat([df_text, new_news]).drop_duplicates(subset=["publishedAt"]).sort_values("publishedAt").reset_index(drop=True)
|
| 191 |
+
print(f"News: {old_txt} → {len(df_text)}")
|
| 192 |
if persist:
|
| 193 |
df_text.to_csv("data/live_news_data.csv", index=False)
|
| 194 |
+
dvc_add_and_push("data/live_news_data.csv")
|
| 195 |
|
| 196 |
+
# === PREDICTION ===
|
| 197 |
if len(df_prices) > 0:
|
| 198 |
df_prices["date"] = df_prices["Date"].dt.date
|
| 199 |
daily_tot = df_text.groupby("date")["sentiment"].mean().reset_index() if len(df_text)>0 else pd.DataFrame()
|
|
|
|
| 205 |
merged["sentiment_lag1"] = merged.groupby("Ticker")["sentiment"].shift(1).bfill().fillna(0)
|
| 206 |
|
| 207 |
for t in ["AAPL", "GOOGL", "TSLA"]:
|
| 208 |
+
if t not in model_info: continue
|
|
|
|
| 209 |
latest = merged[merged["Ticker"] == t].sort_values("Date").iloc[-1]
|
| 210 |
+
point = {"return_lag1": latest["Return"], "volume_lag1": latest["Volume"], "sentiment_lag1": latest["sentiment"]}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
live_predict(t, point, model_info[t])
|
| 212 |
else:
|
| 213 |
+
print("No data to predict")
|
| 214 |
|
| 215 |
time.sleep(FETCH_INTERVAL_SECONDS)
|
| 216 |
|
| 217 |
+
# === 7. TORCH HELPERS ===
|
| 218 |
class TSDataset(Dataset):
|
| 219 |
+
def __init__(self, X, y): self.X, self.y = torch.tensor(X, dtype=torch.float32), torch.tensor(y, dtype=torch.float32)
|
| 220 |
+
def __len__(self): return len(self.X)
|
| 221 |
+
def __getitem__(self, idx): return self.X[idx], self.y[idx]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
def train_torch(model, loader, epochs=50):
|
| 224 |
+
crit, opt = nn.MSELoss(), optim.Adam(model.parameters(), lr=0.001)
|
| 225 |
+
for _ in range(epochs):
|
|
|
|
| 226 |
model.train()
|
| 227 |
+
for xb, yb in loader:
|
| 228 |
opt.zero_grad()
|
| 229 |
+
loss = crit(model(xb), yb.unsqueeze(1))
|
|
|
|
| 230 |
loss.backward()
|
| 231 |
opt.step()
|
| 232 |
|
|
|
|
| 234 |
model.eval()
|
| 235 |
preds = []
|
| 236 |
with torch.no_grad():
|
| 237 |
+
for xb, _ in loader:
|
| 238 |
+
preds.extend(model(xb).squeeze().numpy())
|
|
|
|
| 239 |
return np.array(preds)
|
| 240 |
|
| 241 |
+
# === 8. MODELS ===
|
| 242 |
class MLPModel(nn.Module):
|
| 243 |
+
def __init__(self, input_size): super().__init__(); self.net = nn.Sequential(nn.Linear(input_size,50),nn.ReLU(),nn.Linear(50,25),nn.ReLU(),nn.Linear(25,1))
|
| 244 |
+
def forward(self, x): return self.net(x)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
class LSTMModel(nn.Module):
|
| 247 |
+
def __init__(self, input_size, hidden_size=50, num_layers=2):
|
| 248 |
super().__init__()
|
| 249 |
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
|
| 250 |
self.fc = nn.Linear(hidden_size, 1)
|
| 251 |
+
def forward(self, x): out, _ = self.lstm(x); return self.fc(out[:, -1, :])
|
| 252 |
+
|
| 253 |
+
def create_sequences(X, y, L): xs, ys = [], []; [xs.append(X[i:i+L]) or ys.append(y[i+L]) for i in range(len(X)-L)]; return np.array(xs), np.array(ys)
|
| 254 |
|
| 255 |
+
# === 9. TRAIN ONE TICKER ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
def run_models_for_ticker(ticker, df_merged, seq_length=10):
|
| 257 |
with mlflow.start_run(run_name=f"{ticker}_models"):
|
| 258 |
+
df_t = df_merged[df_merged['Ticker'] == ticker].copy().sort_values('Date')
|
| 259 |
+
df_t['return_lag1'] = df_t['Return'].shift(1); df_t['volume_lag1'] = df_t['Volume'].shift(1); df_t.dropna(inplace=True)
|
| 260 |
+
df_t['target_return'] = df_t['Return'].shift(-1); df_t.dropna(inplace=True)
|
| 261 |
+
X, y = df_t[['return_lag1','volume_lag1','sentiment_lag1']].values, df_t['target_return'].values
|
| 262 |
+
sx, sy = MinMaxScaler(), MinMaxScaler()
|
| 263 |
+
Xs, ys = sx.fit_transform(X), sy.fit_transform(y.reshape(-1,1)).flatten()
|
| 264 |
+
split = int(0.8 * len(Xs))
|
| 265 |
+
Xtr, Xte, ytr, yte = Xs[:split], Xs[split:], ys[:split], ys[split:]
|
| 266 |
+
|
| 267 |
+
# RF
|
| 268 |
+
rf = RandomForestRegressor(n_estimators=200, random_state=42); rf.fit(Xtr, ytr)
|
| 269 |
+
rf_path = f'saved_models/{ticker}_rf.joblib'; joblib.dump(rf, rf_path)
|
| 270 |
+
with mlflow.start_run(run_name=f"{ticker}_RF", nested=True): mlflow.sklearn.log_model(rf, "rf")
|
| 271 |
+
|
| 272 |
+
# MLP
|
| 273 |
+
mlp = MLPModel(X.shape[1]); train_torch(mlp, DataLoader(TSDataset(Xtr, ytr), batch_size=32, shuffle=False))
|
| 274 |
+
mlp_path = f'saved_models/{ticker}_mlp.pth'; torch.save(mlp.state_dict(), mlp_path)
|
| 275 |
+
with mlflow.start_run(run_name=f"{ticker}_MLP", nested=True): mlflow.pytorch.log_model(mlp, "mlp")
|
| 276 |
+
|
| 277 |
+
# LSTM
|
| 278 |
+
lstm_path = None
|
| 279 |
+
Xseq, yseq = create_sequences(Xs, ys, seq_length)
|
| 280 |
+
if len(Xseq) > 10:
|
| 281 |
+
split_seq = int(0.8 * len(Xseq))
|
| 282 |
+
lstm = LSTMModel(X.shape[1]); train_torch(lstm, DataLoader(TSDataset(Xseq[:split_seq], yseq[:split_seq]), batch_size=32, shuffle=False))
|
| 283 |
+
lstm_path = f'saved_models/{ticker}_lstm.pth'; torch.save(lstm.state_dict(), lstm_path)
|
| 284 |
+
with mlflow.start_run(run_name=f"{ticker}_LSTM", nested=True): mlflow.pytorch.log_model(lstm, "lstm")
|
| 285 |
+
|
| 286 |
+
# Save models to DVC
|
| 287 |
+
dvc_add_and_push("saved_models")
|
| 288 |
+
|
| 289 |
+
return {"scaler_X": sx, "scaler_y": sy, "rf_path": rf_path, "mlp_path": mlp_path, "lstm_path": lstm_path, "input_size": X.shape[1]}
|
| 290 |
+
|
| 291 |
+
# === 10. LIVE PREDICT ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
def live_predict(ticker, point, info):
|
| 293 |
Xnew = np.array([[point["return_lag1"], point["volume_lag1"], point["sentiment_lag1"]]])
|
| 294 |
Xs = info["scaler_X"].transform(Xnew)
|
| 295 |
rf = joblib.load(info["rf_path"])
|
| 296 |
pred_rf = info["scaler_y"].inverse_transform(rf.predict(Xs).reshape(-1,1)).flatten()[0]
|
| 297 |
+
mlp = MLPModel(info["input_size"]); mlp.load_state_dict(torch.load(info["mlp_path"])); mlp.eval()
|
| 298 |
+
pred_mlp = info["scaler_y"].inverse_transform(mlp(torch.tensor(Xs, dtype=torch.float32)).detach().numpy().reshape(-1,1)).flatten()[0]
|
|
|
|
|
|
|
| 299 |
pred_lstm = np.nan
|
| 300 |
if info["lstm_path"]:
|
| 301 |
+
lstm = LSTMModel(info["input_size"]); lstm.load_state_dict(torch.load(info["lstm_path"])); lstm.eval()
|
| 302 |
+
seq = np.repeat(Xs, SEQ_LENGTH, axis=0).reshape(1, SEQ_LENGTH, -1)
|
| 303 |
+
pred_lstm = info["scaler_y"].inverse_transform(lstm(torch.tensor(seq, dtype=torch.float32)).detach().numpy().reshape(-1,1)).flatten()[0]
|
|
|
|
|
|
|
| 304 |
print(f"{ticker} - RF: {pred_rf:.6f}, MLP: {pred_mlp:.6f}, LSTM: {pred_lstm:.6f}")
|
|
|
|
| 305 |
|
| 306 |
+
# === MAIN ===
|
| 307 |
if __name__ == "__main__":
|
| 308 |
+
if PULL_AT_START: dvc_pull()
|
| 309 |
+
|
| 310 |
df_prices = load_stock_data()
|
| 311 |
df_text = load_text_data()
|
| 312 |
df_text['sentiment'] = df_text['text'].apply(simple_sentiment)
|
| 313 |
df_prices['date'] = df_prices['Date'].dt.date
|
| 314 |
+
daily_sent_total = df_text.groupby('date')['sentiment'].mean().reset_index()
|
|
|
|
| 315 |
daily_sent_total['date'] = pd.to_datetime(daily_sent_total['date']).dt.date
|
| 316 |
df_merged = df_prices.merge(daily_sent_total, on='date', how='left')
|
| 317 |
df_merged['sentiment'] = df_merged['sentiment'].ffill().fillna(0)
|
| 318 |
df_merged = df_merged.sort_values(['Ticker', 'Date']).reset_index(drop=True)
|
| 319 |
df_merged['sentiment_lag1'] = df_merged.groupby('Ticker')['sentiment'].shift(1).bfill().fillna(0)
|
| 320 |
+
|
| 321 |
model_info = {}
|
| 322 |
for t in ['AAPL', 'GOOGL', 'TSLA']:
|
| 323 |
print(f"\n=== TRAINING {t} ===")
|
| 324 |
res = run_models_for_ticker(t, df_merged)
|
| 325 |
+
if res: model_info[t] = res
|
| 326 |
+
|
| 327 |
print("\n=== LIVE STREAM STARTED ===")
|
| 328 |
+
stream_live(df_prices, df_text, persist=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
news_articles.csv
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
source,author,title,description,url,publishedAt,content
|
| 2 |
+
Gizmodo.com,Bruce Gil,Dogecoin Has Made It to Wall Street,"The first memecoin ETFs have hit the stock market, legitimizing the once too-online digital currency.",https://gizmodo.com/dogecoin-has-made-it-to-wall-street-2000663698,2025-09-25T15:40:25Z,"Are meme coins a legitimate investment now? Well, Wall Street seems to think so. Last week, REX Financial and Osprey Funds launched the first Dogecoin exchange-traded fund (ETF).
|
| 3 |
+
Basically, an ETF i… [+3310 chars]"
|
| 4 |
+
Business Insider,Theron Mohamed,The world's 10 richest people lost nearly $70 billion in Friday's market rout,"Elon Musk, Jeff Bezos, and Mark Zuckerberg led the wealth declines on Friday as fresh fears of a trade war hammered stocks.",https://www.businessinsider.com/rich-list-trump-trade-war-musk-bezos-zuck-ai-stocks-2025-10,2025-10-13T10:45:50Z,"Larry Ellison is one of the world's richest men, so he can buy anything he wants, like a media company or two. That's what Elon Musk did with Twitter.Anna Moneymaker; Kevin Dietsch / Getty Images
|
| 5 |
+
<u… [+2361 chars]"
|
| 6 |
+
Business Insider,jsor@businessinsider.com (Jennifer Sor),These 13 stocks in a small corner of the market should be on investor radars as earnings season nears,"Small- to-mid-cap industrial stocks are heavily discounted and could outperform in the coming earnings season, UBS said.",https://www.businessinsider.com/stocks-what-to-invest-in-small-cap-industrial-investment-ideas-2025-9,2025-09-25T09:15:01Z,"Earnings season is just around the cornerand there's a handful of stocks in one under-the-radar area of the market investors should be keeping their eye on, UBS told clients this week.
|
| 7 |
+
The bank high… [+6055 chars]"
|
| 8 |
+
MacRumors,Joe Rossignol,Apple's Stock Price Reaches New All-Time High,"Apple's stock price reached a new all-time high today, with shares in the company trading for as much as $263.47 on the intraday market, according to Yahoo Finance.
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
The company's previous intraday high was $260.10, set on December 26, 2024.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
Keep in mi…",https://www.macrumors.com/2025/10/20/apple-stock-new-all-time-high/,2025-10-20T15:12:29Z,"Apple's stock price reached a new all-time high today, with shares in the company trading for as much as $263.47 on the intraday market, according to Yahoo Finance.
|
| 19 |
+
The company's previous intraday h… [+860 chars]"
|
| 20 |
+
Gizmodo.com,Kyle Torpey,Bitcoin Price Slumps as Wall Street Prepares for Crypto ETF Frenzy,It's been a rough week for crypto.,https://gizmodo.com/bitcoin-price-slumps-as-wall-street-prepares-for-crypto-etf-frenzy-2000673653,2025-10-17T17:40:25Z,"Earlier this month, bitcoin was on top of the world, hitting yet another new all-time high of roughly $125,000. Around the same time, applications for a large number of increasingly speculative crypt… [+4095 chars]"
|
| 21 |
+
Kotaku,Kotaku,"Ryzen 7 7800X3D Is Selling for Pennies, AMD Is Clearing Out Stock at an All‑Time Low","It might be the best gaming CPU on the market right now for this price.
|
| 22 |
+
The post Ryzen 7 7800X3D Is Selling for Pennies, AMD Is Clearing Out Stock at an All‑Time Low appeared first on Kotaku.",https://kotaku.com/ryzen-7-7800x3d-is-selling-for-pennies-amd-is-clearing-out-stock-at-an-all‑time-low-2000632485,2025-10-07T12:37:49Z,Gaming at high frame rates with maximum detail settings requires a processor that can keep up with modern graphics cards without creating bottlenecks. AMD’s Ryzen 7 7800X3D has earned its reputation … [+3113 chars]
|
| 23 |
+
Gizmodo.com,AJ Dellinger,Is the AI Conveyor Belt of Capital About to Stop?,Good thing it's only the entire economy propped up by this right now.,https://gizmodo.com/is-the-ai-conveyor-belt-of-capital-about-to-stop-2000671017,2025-10-13T19:40:03Z,The American economy is little more than a big bet on AI. Morgan Stanley investor Ruchir Sharma recently noted that money poured into AI investments now accounts for about 40% of the United States GD… [+12177 chars]
|
| 24 |
+
Yahoo Entertainment,,Should You Buy This Ultra-High Dividend Yield Stock in Preparation For a Market Crash?,,https://consent.yahoo.com/v2/collectConsent?sessionId=1_cc-session_f9a919b7-5677-4a40-aa78-95e9fe03a16a,2025-10-01T00:52:00Z,"If you click 'Accept all', we and our partners, including 238 who are part of the IAB Transparency & Consent Framework, will also store and/or access information on a device (in other words, use … [+714 chars]"
|
| 25 |
+
Yahoo Entertainment,Simply Wall St,3 Reliable Dividend Stocks Yielding Up To 9%,"As U.S. stock indexes continue to rise despite the ongoing government shutdown, investors are keenly observing how these developments might influence market ...",https://finance.yahoo.com/news/3-reliable-dividend-stocks-yielding-173154112.html,2025-10-06T17:31:54Z,"As U.S. stock indexes continue to rise despite the ongoing government shutdown, investors are keenly observing how these developments might influence market dynamics and decision-making by the Federa… [+4794 chars]"
|
| 26 |
+
Yahoo Entertainment,Petr Huřťák,1 High-Flying Stock to Own for Decades and 2 We Turn Down,"Expensive stocks often command premium valuations because the market thinks their business models are exceptional. However, the downside is that high...",https://finance.yahoo.com/news/1-high-flying-stock-own-043836187.html,2025-09-26T04:38:36Z,"Expensive stocks often command premium valuations because the market thinks their business models are exceptional. However, the downside is that high expectations are already baked into their prices,… [+2678 chars]"
|
| 27 |
+
Yahoo Entertainment,Jabin Bastian,1 Large-Cap Stock to Consider Right Now and 2 Facing Challenges,Large-cap stocks usually command their industries because they have the scale to drive market trends. The flip side though is that their sheer size can limit...,https://finance.yahoo.com/news/1-large-cap-stock-consider-044736999.html,2025-10-20T04:47:36Z,Large-cap stocks usually command their industries because they have the scale to drive market trends. The flip side though is that their sheer size can limit growth as expanding further becomes an in… [+2725 chars]
|
| 28 |
+
Yahoo Entertainment,Marco Quiroz-Gutierrez,Billionaire PC tycoon Michael Dell is riding the AI gold rush—and he says the party’s far from over even if eventually ‘there’ll be too many’ data centers,Dell Technologies’ stock is up 39% year to date.,https://finance.yahoo.com/news/billionaire-pc-tycoon-michael-dell-190837156.html,2025-10-08T19:08:37Z,"Michael Dell, who revolutionized the personal computer industry with Dell Technologies in the late 90s and is now the 10th richest person in the world, offered his take on the AI wave his company is … [+2323 chars]"
|
| 29 |
+
3quarksdaily.com,John Allen Paulos,The Paradoxical Efficient Market Hypothesis (2024),"by John Allen Paulos Louis Bachelier Election season has put an increased focus on the stock market, but little attention is ever paid to the Efficient Market Hypothesis (the EMH, for short). As I’ve written in A Mathematician Plays the Stock Market, it is a …",https://3quarksdaily.com/3quarksdaily/2024/09/the-paradoxical-efficient-market-hypothesis.html,2025-10-08T02:11:39Z,"by John Allen Paulos
|
| 30 |
+
Louis Bachelier
|
| 31 |
+
Election season has put an increased focus on the stock market, but little attention is ever paid to the Efficient Market Hypothesis (the EMH, for short). As Iv… [+7443 chars]"
|
| 32 |
+
Barchart.com,Rob Isbitts,Nvidia Stock at 8% of the S&P 500 Index Is a Big Problem for Investors. Let’s Do the Math.,One single stock essentially dictates our financial lives these days.,https://www.barchart.com/story/news/35587356/nvidia-stock-at-8-of-the-s-p-500-index-is-a-big-problem-for-investors-lets-do-the-math,2025-10-21T14:42:22Z,"Remember when energy stocks dominated the S&P 500 Index ($SPX), and one company, Exxon Mobil (XOM), was the single largest weighted component?
|
| 33 |
+
Admittedly, although I was managing money at the ti… [+3457 chars]"
|
| 34 |
+
Business Insider,sobrient@insider.com (Samuel O'Brient),"IBM just helped HSBC pull off the world's first quantum-powered trades, and its stock is jumping",HSBC said it used IBM's quantum tech in bond trading. IBM stock popped on the news as investors cheered real-world use for quantum computing.,https://www.businessinsider.com/ibm-stock-price-quantum-computing-hsbc-bond-trading-markets-2025-9,2025-09-25T15:07:49Z,"The move: IBM stock jumped as much as 5% on Thursday, extending a week of solid gains.
|
| 35 |
+
Shares of the tech company have risen steadily in 2025, and are up 22% year-to-date.
|
| 36 |
+
Why: On Thursday, HSBC … [+1960 chars]"
|
| 37 |
+
Yahoo Entertainment,Radek Strnad,"DraftKings (DKNG) Stock Trades Down, Here Is Why",Shares of fantasy sports and betting company DraftKings (NASDAQ:DKNG) fell 11.2% in the afternoon session after reports of rising competition from prediction...,https://finance.yahoo.com/news/draftkings-dkng-stock-trades-down-185056598.html,2025-09-30T18:50:56Z,Shares of fantasy sports and betting company DraftKings (NASDAQ:DKNG) fell 11.2% in the afternoon session after reports of rising competition from prediction platforms Kalshi and Robinhood sparked in… [+2423 chars]
|
| 38 |
+
Yahoo Entertainment,Simply Wall St,3 Promising Penny Stocks With Market Caps Under $900M,"As the U.S. markets close higher after a volatile week, investors are keenly observing opportunities that might arise amidst fluctuating conditions and...",https://finance.yahoo.com/news/3-promising-penny-stocks-market-120516316.html,2025-10-20T12:05:16Z,"As the U.S. markets close higher after a volatile week, investors are keenly observing opportunities that might arise amidst fluctuating conditions and economic uncertainties. Penny stocks, often see… [+5683 chars]"
|
| 39 |
+
Business Insider,Alice Tecotzky,Elon Musk becomes the world's first $500 billion man,Elon Musk's wealth briefly touched the half-a-trillion-dollar mark on Wednesday thanks in part to Tesla's rising stock price.,https://www.businessinsider.com/elon-musk-worlds-first-500-billion-man-2025-10,2025-10-02T14:36:11Z,"Tesla CEO Elon Musk laughingKirsty Wigglesworth/Pool via REUTERS
|
| 40 |
+
<ul><li>Elon Musk has become the first person worth $500 billion.</li><li>Musk's net worth is closely tied to Tesla stock, which is u… [+1798 chars]"
|
| 41 |
+
Gizmodo.com,Ece Yildirim,"AI Won’t Replace Jobs, but Tech Bros Want You to Be Afraid It Will","He also thinks the AI bubble burst is going to make 2008 feel like ""a golden age.""",https://gizmodo.com/ai-wont-replace-jobs-tech-bros-want-you-terrified-2000670808,2025-10-15T14:20:54Z,"AI is overrated, according to author Cory Doctorow.
|
| 42 |
+
“I don’t think AI can do your job,” Doctorow said, but despite that, bosses across industries are “insatiably horny” about the idea that AI will r… [+7077 chars]"
|
| 43 |
+
Business Insider,Lara O'Reilly,Salesforce challenger Zeta Global is making its biggest-ever acquisition as it looks to corner the loyalty market,"Zeta, which helps marketers attract and retain customers, is doubling down on a strategy to get its clients to use more than one of its services.",https://www.businessinsider.com/martech-company-zeta-global-acquire-marigold-enterprise-business-2025-9,2025-09-30T20:06:01Z,"David Steinberg's Zeta Global is making its 17th acquisition.Zeta Global
|
| 44 |
+
<ul><li>Marketing tech company Zeta Global is making its biggest-ever acquisition.</li><li>It's acquiring the enterprise soft… [+3717 chars]"
|
| 45 |
+
Yahoo Entertainment,Rian Howlett,"Stock market today: Dow sinks 500 points, S&P 500, Nasdaq plummet as Trump threatens 'massive increase' on China tariffs","Markets have had an uncertain week, pulled in different directions by AI demand hopes and US government shutdown worries.",https://finance.yahoo.com/news/live/stock-market-today-dow-sinks-500-points-sp-500-nasdaq-plummet-as-trump-threatens-massive-increase-on-china-tariffs-164432249.html,2025-10-10T16:44:32Z,"US stocks turned lower Friday as President Trump and China traded blows on tariffs, with Trump threatening a ""massive increase"" in tariffs on Chinese goods.
|
| 46 |
+
The Dow Jones Industrial Average (^DJI) l… [+8494 chars]"
|
| 47 |
+
Yahoo Entertainment,Petr Huřťák,"Super Micro (SMCI) Stock Trades Up, Here Is Why",Shares of server solutions provider Super Micro (NASDAQ:SMCI) jumped 3% in the afternoon session after the company unveiled a new lineup of AI-optimized...,https://finance.yahoo.com/news/super-micro-smci-stock-trades-204708134.html,2025-09-22T20:47:08Z,Shares of server solutions provider Super Micro (NASDAQ:SMCI) jumped 3% in the afternoon session after the company unveiled a new lineup of AI-optimized computing systems at its INNOVATE! event in Ma… [+2917 chars]
|
| 48 |
+
Business Insider,Andrea Wasserman,"I rejected and approved hundreds of raises at Nordstrom, Verizon, and Yahoo. Here's my advice for getting a raise in the current market.",Andrea Wasserman says her best advice for securing a raise at your job in this market is to make yourself indispensable.,https://www.businessinsider.com/how-to-get-raise-current-job-market-executive-coach-2025-10,2025-10-17T09:05:01Z,"Andrea Wasserman.Courtesy of Andrea Wasserman
|
| 49 |
+
<ul><li>Executive coach Andrea Wasserman says it's difficult but not impossible to secure a raise right now.</li><li>Companies have less urgency to incr… [+5939 chars]"
|
| 50 |
+
ABC News,"Mary Bruce, Karen Travers, Michelle Stoddart, Max Zahn","Trump threatens 'massive' tariffs on China, triggering stock market sell-off",China imposed limits on rare earth minerals a day earlier.,https://abcnews.go.com/Politics/trump-threatens-massive-tariffs-china-triggering-stock-market/story?id=126405187,2025-10-10T16:50:42Z,"President Donald Trump on Friday voiced frustration with what he called China's ""trade hostility,"" threatening to respond with large tariffs on China and to cancel his upcoming meeting with Chinese P… [+1435 chars]"
|
| 51 |
+
Yahoo Entertainment,Radek Strnad,"AMD (AMD) Stock Is Up, What You Need To Know",Shares of computer processor maker AMD (NASDAQ:AMD) jumped 3.6% in the afternoon session after the company announced an expanded artificial intelligence (AI)...,https://finance.yahoo.com/news/amd-amd-stock-know-172042259.html,2025-10-02T17:20:42Z,Shares of computer processor maker AMD (NASDAQ:AMD) jumped 3.6% in the afternoon session after the company announced an expanded artificial intelligence (AI) partnership and reports surfaced of a pot… [+2545 chars]
|
| 52 |
+
Yahoo Entertainment,Jabin Bastian,Why Lattice Semiconductor (LSCC) Stock Is Trading Up Today,Shares of semiconductor designer Lattice Semiconductor (NASDAQ:LSCC) jumped 3.3% in the morning session after a major AI-focused partnership in the chip...,https://finance.yahoo.com/news/why-lattice-semiconductor-lscc-stock-155601633.html,2025-10-06T15:56:01Z,Shares of semiconductor designer Lattice Semiconductor (NASDAQ:LSCC) jumped 3.3% in the morning session after a major AI-focused partnership in the chip sector boosted sentiment for stocks with expos… [+2598 chars]
|
| 53 |
+
The Verge,Emma Roth,$55 billion EA buyout hands Madden over to investors including Saudi Arabia and Jared Kushner,"Electronic Arts has announced it is being acquired and taken private in a $55 billion deal involving Silver Lake, Saudi Arabia’s Public Investment Fund, and Affinity Partners, which is founded and led by Donald Trump’s son-in-law, Jared Kushner. The deal is e…",https://www.theverge.com/news/787112/electronic-arts-55-billion-privacte-acquisition-pif-silver-lake-affinity-partners,2025-09-29T12:51:27Z,"<ul><li></li><li></li><li></li></ul>
|
| 54 |
+
Silver Lake, PIF, and Affinity Partners will take the gaming giant private.
|
| 55 |
+
Silver Lake, PIF, and Affinity Partners will take the gaming giant private.
|
| 56 |
+
by
|
| 57 |
+
Emm… [+3083 chars]"
|
| 58 |
+
Yahoo Entertainment,Simply Wall St,High Growth Tech Stocks with Promising Global Potential,"Amidst a backdrop of record highs in major U.S. stock indexes and a rally in small-cap stocks following the Federal Reserve's interest rate cut, global...",https://finance.yahoo.com/news/high-growth-tech-stocks-promising-093818624.html,2025-09-23T09:38:18Z,"Amidst a backdrop of record highs in major U.S. stock indexes and a rally in small-cap stocks following the Federal Reserve's interest rate cut, global markets are navigating through significant econ… [+6132 chars]"
|
| 59 |
+
Business Insider,Kwan Wei Kevin Tan,Mark Cuban is advocating for companies to share the wealth with employees,"""Compassion and capitalism, not greed, are what can make this country far greater,"" Cuban wrote on X.",https://www.businessinsider.com/mark-cuban-companies-share-wealth-employees-2025-10,2025-10-13T04:54:26Z,"""Compassion and capitalism, not greed, are what can make this country far greater,"" Mark Cuban wrote on X on Sunday.Julia Beverly/WireImage via Getty Images
|
| 60 |
+
<ul><li>Mark Cuban says that companies sh… [+3469 chars]"
|
| 61 |
+
The New Yorker,John Cassidy,The A.I. Boom and the Spectre of 1929,"As some financial leaders fret publicly about the stock market falling to earth, Andrew Ross Sorkin’s new book recounts the greatest crash of them all.",https://www.newyorker.com/news/the-financial-page/the-ai-boom-and-the-spectre-of-1929,2025-10-13T19:08:24Z,"No two speculative booms are exactly alike, of course, but they share some common elements. Typically, there is great excitement among investors about new technologyin todays case, A.I.and its potent… [+6613 chars]"
|
| 62 |
+
Business Insider,Melia Russell,Inside the battle in legal tech to 'OpenAI-proof' its business,OpenAI's own use of AI put the SaaS market on notice. Legal tech companies are confident they're safe — for now.,https://www.businessinsider.com/legal-tech-saas-openai-competition-crosby-evenup-spellbook-eve-2025-10,2025-10-09T18:10:01Z,"Sam AltmanFlorian Gaertner/Photothek via Getty Images
|
| 63 |
+
<ul><li>Funding for legal tech is off to a $320 million start this month.</li><li>The money is gushing in, even as key questions remain.</li><li… [+4641 chars]"
|
| 64 |
+
Yahoo Entertainment,Simply Wall St,Asian Dividend Stocks To Enhance Your Portfolio,"As global markets navigate a complex landscape marked by economic uncertainties and geopolitical tensions, the Asian stock markets have shown resilience...",https://finance.yahoo.com/news/asian-dividend-stocks-enhance-portfolio-223905812.html,2025-10-05T22:39:05Z,"As global markets navigate a complex landscape marked by economic uncertainties and geopolitical tensions, the Asian stock markets have shown resilience, with indices in China and Japan reflecting mi… [+5544 chars]"
|
| 65 |
+
TheStreet,Moz Farooque,Jamie Dimon has blunt call on the stock market,"So far in 2025, the U.S. stock market has marched forward with considerable aplomb. Case in point: after racking up multiple highs, the S&P 500 is up nearly ...",https://www.thestreet.com/investing/jamie-dimon-has-blunt-call-on-the-stock-market-,2025-10-08T18:56:38Z,"So far in 2025, the U.S. stock market has marched forward with considerable aplomb.
|
| 66 |
+
Case in point: after racking up multiple highs, the S&P 500 is up nearly 17% year-to-date, and multiple sector… [+4594 chars]"
|
| 67 |
+
Yahoo Entertainment,Ramzan Karmali,Market Movers: The rise of China's AI,Chinese tech stocks like Alibaba (BABA) and Tencent (0700.HK) have been on the up as optimism about developments they're making in Artificial Intelligence...,https://finance.yahoo.com/video/market-movers-rise-chinas-ai-122822192.html,2025-09-30T12:28:22Z,"Can China threaten the US dominance in AI? Well, the Shanghai composite, China's mainland benchmark rose 3% in September alone. It's longest monthly rally since 2017. Why? Well, Asian tech stocks hav… [+6529 chars]"
|
| 68 |
+
Yahoo Entertainment,Zacks Equity Research,Why This 1 Value Stock Could Be a Great Addition to Your Portfolio,The Zacks Style Scores offers investors a way to easily find top-rated stocks based on their investing style. Here's why you should take advantage.,https://finance.yahoo.com/news/why-1-value-stock-could-134004930.html,2025-09-30T13:40:04Z,"Taking full advantage of the stock market and investing with confidence are common goals for new and old investors alike.
|
| 69 |
+
While you may have an investing style you rely on, finding great stocks is m… [+2094 chars]"
|
| 70 |
+
Harvard Business Review,"Jenny Fernandez, Kathryn Landis",How to Lead When the Conditions for Success Suddenly Disappear,"Broaden coalitions, build resiliency, and reset expectations.",https://hbr.org/2025/10/how-to-lead-when-the-conditions-for-success-suddenly-disappear,2025-10-13T12:05:36Z,"In todays workplace, uncertainty has become the norm. Economic headwinds and a volatile stock market have been prompting companies to slash or reallocate budgets mid-course. Generative AI and other f… [+343 chars]"
|
| 71 |
+
Slashdot.org,EditorDavid,'Circular' AI Mega-Deals by AI and Hardware Giants are Raising Eyebrows,"""Nvidia is investing billions in and selling chips to OpenAI, which is also buying chips from and earning stock in AMD,"" writes SFGate. ""AMD sells processors to Oracle, which is building data centers with OpenAI — which also gets data center work from CoreWea…",https://slashdot.org/story/25/10/11/1819237/circular-ai-mega-deals-by-ai-and-hardware-giants-are-raising-eyebrows,2025-10-11T18:34:00Z,"""Nvidia is investing billions in and selling chips to OpenAI, which is also buying chips from and earning stock in AMD,"" writes SFGate. ""AMD sells processors to Oracle, which is building data centers… [+1630 chars]"
|
| 72 |
+
Business Insider,Theron Mohamed,Top strategist Paul Dietrich shares 2 picks to ride out an AI crash,"Wedbush's Paul Dietrich said the AI boom reminds him of the dot-com and housing bubbles, and he's bullish on gold and utilities.",https://www.businessinsider.com/wedbush-dietrich-ai-boom-stock-market-bubble-recession-gold-utilities-2025-10,2025-10-09T09:00:01Z,"Wedbush's Paul Dietrich is worried about the stock market and economy.Osmancan Gurdogan/Anadolu via Getty Images
|
| 73 |
+
<ul><li>Paul Dietrich shared two alternative investments to AI, saying the tech boom … [+3628 chars]"
|
| 74 |
+
Yahoo Entertainment,Nauman khan,PLTR: Palantir Breaks $1 Billion Revenue Milestone--Is This Just the Beginning?,Defense And AI Deals Power Palantir Stock To Record-Breaking Quarter,https://finance.yahoo.com/news/pltr-palantir-breaks-1-billion-183512308.html,2025-10-03T18:35:12Z,"This article first appeared on GuruFocus.
|
| 75 |
+
Oct 3 - Palantir Technologies (NASDAQ:PLTR) is turning heads again, not just because of the AI hype cycle, but because the company is stacking real wins tha… [+1539 chars]"
|
| 76 |
+
Yahoo Entertainment,Anthony Lee,2 Safe-and-Steady Stocks to Consider Right Now and 1 We Brush Off,"A stock with low volatility can be reassuring, but it doesn’t always mean strong long-term performance. Investors who prioritize stability may miss out on...",https://finance.yahoo.com/news/2-safe-steady-stocks-consider-043322597.html,2025-10-16T04:33:22Z,"A stock with low volatility can be reassuring, but it doesnt always mean strong long-term performance. Investors who prioritize stability may miss out on higher-reward opportunities elsewhere.
|
| 77 |
+
Lucki… [+2601 chars]"
|
| 78 |
+
Barchart.com,Wajeeh Khan,Plug Power Just Reported Record Output. Should You Buy PLUG Stock Here?,Plug Power shares briefly rallied after the hydrogen fuel cell specialist reported record production at its Georgia facility. But PLUG stock may still be a...,https://www.barchart.com/story/news/34988069/plug-power-just-reported-record-output-should-you-buy-plug-stock-here,2025-09-23T18:54:38Z,"Plug Power (PLUG) shares pushed notably to the upside today after the clean energy firm said its Georgia green hydrogen facility reached record production of 324 metric tons in August 2025.
|
| 79 |
+
The hydr… [+2169 chars]"
|
| 80 |
+
Business Insider,Lee Chong Ming,Satya Nadella is netting a record $96.5 million pay package as Microsoft's AI bets paid off,Microsoft CEO Satya Nadella's pay soared to a record $96.5 million as the company's revenue hit $281 billion this year.,https://www.businessinsider.com/satya-nadella-96-million-pay-salary-microsoft-ai-filing-2025-10,2025-10-22T04:49:44Z,"Satya Nadella's record $96.5 million payday shows how Microsoft's AI boom is rewarding its top leader.JASON REDMOND/AFP via Getty Images
|
| 81 |
+
<ul><li>Satya Nadella's pay package rose to a record $96.5 mi… [+2519 chars]"
|
| 82 |
+
Yahoo Entertainment,Adam Hejl,1 Stock Under $50 Worth Investigating and 2 We Question,The $10-50 price range often includes mid-sized businesses with proven track records and plenty of growth runway ahead. They also usually carry less risk...,https://finance.yahoo.com/news/1-stock-under-50-worth-043128393.html,2025-10-20T04:31:28Z,"The $10-50 price range often includes mid-sized businesses with proven track records and plenty of growth runway ahead. They also usually carry less risk than penny stocks, though theyre not immune t… [+2448 chars]"
|
| 83 |
+
Barchart.com,Jim Van Meerten,"This Quantum Computing Stock Is Up 4,024% in a Year. What Comes Next?","Rigetti Computing (RGTI) stands out for its quantum-classical computing infrastructure and has surged 4,024% over the past year. RGTI is trading above key...",https://www.barchart.com/story/news/35009546/this-quantum-computing-stock-is-up-4-024-in-a-year-what-comes-next,2025-09-24T14:50:03Z,"<ul><li>Rigetti Computing (RGTI) stands out for its quantum-classical computing infrastructure and has surged 4,024% over the past year.
|
| 84 |
+
</li><li>RGTI is trading above key moving averages and has a … [+4557 chars]"
|
| 85 |
+
Yahoo Entertainment,Simply Wall St,Rocket Lab Shares Soar 591% as Investors Weigh Valuation After NASA Contract Win,"If you’re looking at Rocket Lab’s stock chart this year, you’re probably double-checking your math right now. After all, a 591.1% gain over the last 12...",https://finance.yahoo.com/news/rocket-lab-shares-soar-591-050355002.html,2025-10-09T05:03:55Z,"If youre looking at Rocket Labs stock chart this year, youre probably double-checking your math right now. After all, a 591.1% gain over the last 12 months and a staggering 1,532.8% jump over three y… [+6990 chars]"
|
| 86 |
+
Yahoo Entertainment,Syeda Seirut Javed,Jim Cramer on Apollo Global: “This is the Toughest One for Me to Swallow”,"Apollo Global Management, Inc. (NYSE:APO) is one of the stocks Jim Cramer was recently focused on. Cramer noted that the stock has been hit “much harder than...",https://finance.yahoo.com/news/jim-cramer-apollo-global-toughest-100340902.html,2025-10-03T10:03:40Z,"Apollo Global Management, Inc. (NYSE:APO) is one of the stocks Jim Cramer was recently focused on. Cramer noted that the stock has been hit much harder than the others, as he remarked:
|
| 87 |
+
Finally, ther… [+1174 chars]"
|
| 88 |
+
BBC News,,"UK will be second-fastest-growing G7 economy, IMF predicts",The International Monetary Fund (IMF) has upgraded the rate at which the UK economy will grow this year.,https://www.bbc.com/news/articles/cn092p27xn0o,2025-10-14T13:00:40Z,"Faisal IslamBBC Economics Editor
|
| 89 |
+
Chancellor Rachel Reeves said, despite the IMF's upgrade to UK economic growth, ""for too many people, our economy feels stuck""
|
| 90 |
+
The UK is forecast to be the second-f… [+4476 chars]"
|
| 91 |
+
Kotaku,Mike Fazioli,"Amazon Slashes AMD Ryzen 7 Mini Gaming PC to Lowest Price, Likely Clearing Stock Before Prime Big Deal Days","The BOSGAME P3 brings impressive gaming power to the work-centric mini PC craze, and you can save $150 if you act now.
|
| 92 |
+
The post Amazon Slashes AMD Ryzen 7 Mini Gaming PC to Lowest Price, Likely Clearing Stock Before Prime Big Deal Days appeared first on Kotak…",https://kotaku.com/amazon-slashes-amd-ryzen-7-mini-gaming-pc-to-lowest-price-likely-clearing-stock-before-prime-big-deal-days-2000629670,2025-09-29T15:10:25Z,"When we look back on 2025, we might remember it as the Year of the Mini PC. These mighty mites broke out in a huge way this year, with powerful and fast new models flooding the market and putting to … [+2497 chars]"
|
| 93 |
+
Business Insider,Daniel Geiger,A $5 billion deal key to CoreWeave's AI empire just lost another investor's support,CoreWeave's deal to acquire a data center partner is in jeopardy after shareholders oppose the transaction.,https://www.businessinsider.com/core-scientific-shareholder-oppose-coreweave-takeover-bid-2025-10,2025-10-17T21:44:09Z,"Michael Intrator, CoreWeave's CEOTom Williams/CQ-Roll Call, Inc via Getty Images
|
| 94 |
+
<ul><li>CoreWeave's rapid growth has thrust it into the center of a growing debate about an AI bubble.</li><li>In Jul… [+7588 chars]"
|
| 95 |
+
Yahoo Entertainment,Rian Howlett,"Stock market today: Dow sinks 800 points, S&P 500 and Nasdaq see worst day since April as Trump's renewed tariff threats spook Wall Street",Markets tumble to end the week as President Trump renewed tariff threats on China.,https://finance.yahoo.com/news/live/stock-market-today-dow-sinks-800-points-sp-500-and-nasdaq-see-worst-day-since-april-as-trumps-renewed-tariff-threats-spook-wall-street-200026352.html,2025-10-10T20:00:26Z,"US stocks closed sharply lower on Friday as President Trump and China traded blows on tariffs, with Trump threatening a ""massive increase"" in duties on Chinese goods.
|
| 96 |
+
The Dow Jones Industrial Averag… [+11161 chars]"
|
| 97 |
+
Yahoo Entertainment,Radek Strnad,WD-40 (WDFC) Reports Q3: Everything You Need To Know Ahead Of Earnings,Household products company WD-40 (NASDAQ:WDFC) will be reporting results this Wednesday after market hours. Here’s what investors should know.,https://finance.yahoo.com/news/wd-40-wdfc-reports-q3-030612703.html,2025-10-21T03:06:12Z,"Household products company WD-40 (NASDAQ:WDFC) will be reporting results this Wednesday after market hours. Heres what investors should know.
|
| 98 |
+
WD-40 missed analysts revenue expectations by 2.3% last … [+2214 chars]"
|
| 99 |
+
Kotaku,Joe Tilleli,This Marshall Portable Bluetooth Speaker Drops Even Lower Than Its Prime Day Price as Amazon Quietly Clears Out Stock,"Nearly 50% off on the Marshall Emberton II portable Bluetooth speaker in black and brass.
|
| 100 |
+
The post This Marshall Portable Bluetooth Speaker Drops Even Lower Than Its Prime Day Price as Amazon Quietly Clears Out Stock appeared first on Kotaku.",https://kotaku.com/this-marshall-portable-bluetooth-speaker-drops-even-lower-than-its-prime-day-price-as-amazon-quietly-clears-out-stock-2000634630,2025-10-14T14:10:36Z,"Amazon’s Prime Big Deals Days has come and gone. This was the online retail giant’s two-day sales event where you’re able to save big on TVs, tablets, coffee makers, and pretty much anything else. Th… [+2194 chars]"
|
| 101 |
+
Business Insider,Aditi Bharade,An early Pop Mart investor said Chinese brands that want to make it big need to set their eyes on one market early on,David He from Black Ant Capital said Chinese companies should set their eyes on the US early on.,https://www.businessinsider.com/pop-mart-investor-chinese-brands-us-market-early-2025-10,2025-10-09T04:41:42Z,"An early Pop Mart investor said there is one market Chinese brands need to gun for early on.Costfoto/NurPhoto via Getty Images
|
| 102 |
+
<ul><li>A Pop Mart investor said Chinese companies that want to succeed… [+2270 chars]"
|
| 103 |
+
Kotaku,Kotaku Deals,"Amazon Clears Out Govee Smart Lamp Stock, Now Selling for Less Than a Basic Floor Lamp","A smart lamp completely transforms the atmosphere.
|
| 104 |
+
The post Amazon Clears Out Govee Smart Lamp Stock, Now Selling for Less Than a Basic Floor Lamp appeared first on Kotaku.",https://kotaku.com/amazon-clears-out-govee-smart-lamp-stock-now-selling-for-less-than-a-basic-floor-lamp-2000632160,2025-10-07T00:35:59Z,Your living room deserves smart lighting that doesn’t drain your wallet. Smart floor lamps have become incredibly popular for transforming spaces with dynamic colors and voice control but the price t… [+3004 chars]
|
| 105 |
+
Slashdot.org,EditorDavid,Three-Wheeled Solar Car Maker Aptera is About to Go Public,"Last November Aptera successfully tested its first production-intent three-wheel solar-powered EV — and said it already had over 50,000 reservations. The vehicles had a solar charge range of 40 miles per day, reported Digital Trends, noting the crowdfunded co…",https://tech.slashdot.org/story/25/10/12/0237231/three-wheeled-solar-car-maker-aptera-is-about-to-go-public,2025-10-12T16:34:00Z,"Last November Aptera successfully tested its first production-intent three-wheel solar-powered EV — and said it already had over 50,000 reservations. The vehicles had a solar charge range of 40 miles… [+1848 chars]"
|
| 106 |
+
Yahoo Entertainment,Nauman khan,NVDA: This Analyst Sees Massive 80% Upside For Nvidia Stock -- Here's Why,HSBC Predicts Nvidia's Market Cap Could Double To $8 Trillion,https://finance.yahoo.com/news/nvda-analyst-sees-massive-80-183637187.html,2025-10-16T18:36:37Z,"This article first appeared on GuruFocus.
|
| 107 |
+
Oct 16 - Nvidia (NASDAQ:NVDA) gained fresh bullish momentum after HSBC raised its rating on the chipmaker to buy from hold, pointing to sustained demand for… [+1078 chars]"
|
| 108 |
+
Yahoo Entertainment,Anthony Richardson,The Weekend: IMF warns of global debt emergency as Reeves grapples with slowing jobs market,"Key moments from the last seven days, plus a glimpse at the week ahead.",https://uk.finance.yahoo.com/news/imf-global-debt-reeves-jobs-economy-weekend-budget-125851835.html,2025-10-18T12:58:51Z,There was a stark warning from the IMF this week as it predicted government debt across the world would hit the highest level since the aftermath of World War Two by the end of the decade. According … [+6551 chars]
|
| 109 |
+
Gizmodo.com,Lucas Ropek,Is the Founder of Polymarket Really the Youngest Self-Made Billionaire?,Shayne Coplan is having a good year.,https://gizmodo.com/is-the-founder-of-polymarket-really-the-youngest-self-made-billionaire-2000669910,2025-10-08T21:10:38Z,"Polymarket, the popular prediction market, has been doing really well lately. In fact, it’s been doing so well that Bloomberg is out with a new story that claims the site’s founder, Shayne Coplan, is… [+4592 chars]"
|
| 110 |
+
Kotaku,Kotaku Deals,"Sony’s 1000XM4 Is Now 3 Times Cheaper Than the AirPods Max, Amazon Is Clearing Out Stock for Prime Day","There’s no reason to pay over twice this price for the 1000XM6 either.
|
| 111 |
+
The post Sony’s 1000XM4 Is Now 3 Times Cheaper Than the AirPods Max, Amazon Is Clearing Out Stock for Prime Day appeared first on Kotaku.",https://kotaku.com/sonys-1000xm4-is-now-3-times-cheaper-than-the-airpods-max-amazon-is-clearing-out-stock-for-prime-day-2000631836,2025-10-05T22:45:58Z,"The high-end noise-canceling headphone market is more crowded than, say, Times Square during New Year’s Eve celebrations. Apple, Bose, Sonos, Sennheiser, and Sony are all fighting for your attention,… [+3013 chars]"
|
| 112 |
+
Yahoo Entertainment,Simply Wall St,How Recent Changes Are Shaping the Starbucks Investment Story,"Starbucks stock has recently seen its Fair Value Estimate revised downward, moving modestly from $99.38 to $97.63 per share. This adjustment comes as...",https://finance.yahoo.com/news/recent-changes-shaping-starbucks-investment-050454994.html,2025-10-09T05:04:54Z,"Starbucks stock has recently seen its Fair Value Estimate revised downward, moving modestly from $99.38 to $97.63 per share. This adjustment comes as analysts weigh optimism around operational improv… [+5938 chars]"
|
| 113 |
+
Kotaku,Kotaku Deals,Bose Is Quietly Selling Its QC Headphones at a New Record Low After Prime Day to Clear Out Stock,"At this price, Bose could empty every warehouse of its iconic ANC headphones.
|
| 114 |
+
The post Bose Is Quietly Selling Its QC Headphones at a New Record Low After Prime Day to Clear Out Stock appeared first on Kotaku.",https://kotaku.com/bose-is-quietly-selling-its-qc-headphones-at-a-new-record-low-after-prime-day-to-clear-out-stock-2000634117,2025-10-10T22:35:00Z,"Amazon’s October answer to the summer Prime Day event (Prime Big Deal Day) just wrapped up but some of the best deals are still hanging around. The Bose QuietComfort headphones are a perfect example,… [+3351 chars]"
|
| 115 |
+
Barchart.com,Jabran Kundi,"Dear Intel Stock Fans, Mark Your Calendars for October 23","The upcoming earnings could bring a positive surprise, but everyone has their eyes on the company's product roadmap.",https://www.barchart.com/story/news/35588511/dear-intel-stock-fans-mark-your-calendars-for-october-23,2025-10-21T15:43:43Z,"Intel (INTC) is expected to announce its Q3 earnings report later this week, and unlike the last few times, there is visibly more optimism among investors. It is not difficult to figure out why. The … [+5348 chars]"
|
| 116 |
+
Business Insider,Theron Mohamed,"People are chasing AI stocks like 'dogs chase cars' — and a crash looks certain, veteran investor Bill Smead says","Bill Smead said ""momentum"" is fueling the AI boom, the tech's potential is already priced in, and it's almost a replay of the dot-com bubble.",https://www.businessinsider.com/ai-stocks-bubble-crash-smead-market-outlook-nvidia-openai-tech-2025-9,2025-09-30T14:29:23Z,"Bill SmeadChristopher Goodney/Bloomberg via Getty Images
|
| 117 |
+
<ul><li>The AI boom is being fueled by ""momentum"" as investors chase huge stock gains, Bill Smead said.</li><li>The value investor said surgi… [+3090 chars]"
|
| 118 |
+
Yahoo Entertainment,Simply Wall St,Polestar (NasdaqGM:PSNY): Assessing Valuation as Shares Continue Recent Slide,"Polestar Automotive Holding UK (NasdaqGM:PSNY) shares dipped 3% in the past day, continuing a challenging trend for the EV maker. Over the past month, the...",https://finance.yahoo.com/news/polestar-nasdaqgm-psny-assessing-valuation-070820516.html,2025-10-20T07:08:20Z,"Polestar Automotive Holding UK (NasdaqGM:PSNY) shares dipped 3% in the past day, continuing a challenging trend for the EV maker. Over the past month, the stock has dropped 10%. This reflects ongoing… [+3881 chars]"
|
| 119 |
+
Yahoo Entertainment,Anirudha Bhagat,NVDA vs. AMD: Which AI Hardware Stock Has Better Investment Potential?,"NVIDIA's dominance in AI chips, booming data center sales and expanding partnerships give it an edge over AMD in the race for AI hardware leadership.",https://finance.yahoo.com/news/nvda-vs-amd-ai-hardware-123500021.html,2025-10-15T12:35:00Z,"NVIDIA Corporation NVDA and Advanced Micro Devices, Inc. AMD sit at the center of the artificial intelligence (AI) hardware revolution. Both are crucial players in the semiconductor space, competing … [+5531 chars]"
|
| 120 |
+
Yahoo Entertainment,Reuters,India's stock benchmarks set to open flat after 7-session losing run,"Both the Nifty and the BSE Sensex have lost about 3.1% in the last seven sessions, their longest losing streak in nearly seven months. ""Persistent foreign...",https://sg.finance.yahoo.com/news/indias-stock-benchmarks-set-open-021844170.html,2025-09-30T02:18:44Z,"(Reuters) -India's equity benchmarks are poised to open little changed on Tuesday after falling for seven straight sessions, hurt by steep U.S. tariffs and a hike in the H-1B visa fee, while caution … [+2060 chars]"
|
| 121 |
+
Barchart.com,Sristi Suman Jayaswal,"Plug Power Just Named a New CEO. Should You Buy, Sell, or Hold PLUG Stock Here?",Plug Power is back in the spotlight with fresh leadership news.,https://www.barchart.com/story/news/35463129/plug-power-just-named-a-new-ceo-should-you-buy-sell-or-hold-plug-stock-here,2025-10-15T13:00:02Z,"Hydrogen might not make daily headlines, but its quietly powering the clean energy revolution. From fueling futuristic vehicles to driving heavy industry, global demand hit 97 million metric tons in … [+5956 chars]"
|
| 122 |
+
Yahoo Entertainment,Simply Wall St,Is Applied Materials’ Surge Justified After US Chip Investment News in 2025?,"If you are weighing your next move on Applied Materials, you are not alone. The stock has been on quite a ride lately, climbing an impressive 17.3% in just...",https://finance.yahoo.com/news/applied-materials-surge-justified-us-110636082.html,2025-09-23T11:06:36Z,"If you are weighing your next move on Applied Materials, you are not alone. The stock has been on quite a ride lately, climbing an impressive 17.3% in just the past week and soaring 23.4% over the la… [+6820 chars]"
|
| 123 |
+
Yahoo Entertainment,Mediaite,CNBC Star 'Anxious' Wall Street Is 'Reliving' 1929 Crash,CNBC star Andrew Ross Sorkin said he is anxious Wall Street is racing towards a stock market crash like the one that rocked investors in 1929.,https://www.yahoo.com/news/videos/cnbc-star-anxious-wall-street-004026325.html,2025-10-13T00:40:26Z,CNBC star Andrew Ross Sorkin said he is anxious Wall Street is racing towards a stock market crash like the one that rocked investors in 1929.
|
| 124 |
+
Yahoo Entertainment,Yahoo Finance Video,"SAP probe, Jabil falls, Birkenstock outlook: Trending Tickers","Market Catalysts host Julie Hyman tracks several of the day's top trending stock tickers, including the European Union's (EU) antitrust probe into SAP (SAP),...",https://finance.yahoo.com/video/sap-probe-jabil-falls-birkenstock-144823059.html,2025-09-25T14:48:23Z,"Now time for some of today's trending tickers. We are watching SAP, Jable and Birkenstock. First up, SAP. The European Commission launched an anti-trust probe into the German software giant, citing c… [+1385 chars]"
|
| 125 |
+
Business Insider,Bradley Saacks,"Hedge fund September returns: Here's how Citadel, Balyasny, and ExodusPoint have performed so far this year",Big-name funds were mostly unable to keep up with the S&P 500's gains in September.,https://www.businessinsider.com/september-hedge-funds-performance-citadel-balyasny-2025-10,2025-10-01T18:25:55Z,"Citadel's Ken GriffinMichael M. Santiago/Getty Images
|
| 126 |
+
<ul><li>Multistrategy hedge funds were mostly positive in September.</li><li>Managers like Citadel, Balyasny, and ExodusPoint added to their gai… [+2110 chars]"
|
| 127 |
+
Kotaku,Kotaku Deals,"Roborock Saros 10R $1,600 Robot Vacuum Is Now Selling for Peanuts, Amazon Is Quietly Clearing Out All Its Stock","The Roborock Saros 10R ranks among the world’s top three robot vacuums with mops for performance.
|
| 128 |
+
The post Roborock Saros 10R $1,600 Robot Vacuum Is Now Selling for Peanuts, Amazon Is Quietly Clearing Out All Its Stock appeared first on Kotaku.",https://kotaku.com/roborock-saros-10r-1600-robot-vacuum-is-now-selling-for-peanuts-amazon-is-quietly-clearing-out-all-its-stock-2000633116,2025-10-08T10:13:50Z,"Robot vacuums have evolved far beyond the basic “bump-and-spin” models that once dominated store shelves at budget prices. Roborock has firmly planted itself in the luxury segment of this market, whe… [+3467 chars]"
|
| 129 |
+
Yahoo Entertainment,Zacks Equity Research,Why Duke Energy Stock Deserves a Spot in Your Portfolio Right Now,DUK's $190-$200 billion investment plan and expanding renewable portfolio make it a standout utility stock pick.,https://finance.yahoo.com/news/why-duke-energy-stock-deserves-122400496.html,2025-10-08T12:24:00Z,Duke Energy DUK continues to invest consistently in infrastructure and expansion projects to enhance service reliability for its customers. The company is also progressively increasing its renewable … [+3882 chars]
|
| 130 |
+
Business Insider,katherineli@insider.com (Katherine Li),Peloton jacks membership prices and leans into AI in post-pandemic comeback push,Peloton is attempting a post-pandemic comeback with higher membership prices and AI-powered hardware among its new Cross Training Series.,https://www.businessinsider.com/peloton-cross-training-series-membership-price-increase-2025-10,2025-10-01T22:39:44Z,"Peloton is aiming for a grand comeback ahead of the holidays.
|
| 131 |
+
The company has been struggling after its meteoric rise during the pandemic.
|
| 132 |
+
Its latest release? A $6,695 treadmill equipped with AI, a… [+2453 chars]"
|
| 133 |
+
Business Insider,Dan DeFrancesco,Jimmy Kimmel's return doesn't mean the end of Disney's problems,"Now that Jimmy Kimmel's show has become a lightning rod, any decision about it going forward will be viewed with a magnifying glass.",https://www.businessinsider.com/jimmy-kimmel-live-returns-disney-problems-2025-9,2025-09-23T13:46:23Z,"Host Jimmy Kimmel speaks onstage during the 95th Annual Academy Awards at Dolby Theatre on March 12, 2023 in Hollywood, California.Kevin Winter/Getty Images
|
| 134 |
+
<ul><li>This post originally appeared in … [+8291 chars]"
|
| 135 |
+
Yahoo Entertainment,Radek Strnad,1 Unpopular Stock That Deserves Some Love and 2 We Brush Off,Wall Street’s bearish price targets for the stocks in this article signal serious concerns. Such forecasts are uncommon in an industry where maintaining...,https://finance.yahoo.com/news/1-unpopular-stock-deserves-love-043920383.html,2025-10-20T04:39:20Z,Wall Streets bearish price targets for the stocks in this article signal serious concerns. Such forecasts are uncommon in an industry where maintaining cordial corporate relationships often trumps de… [+2801 chars]
|
| 136 |
+
Business Insider,Thibault Spirlet,An economist and ex-central banker shares 5 investing rules to help Gen Z and millennials build wealth — and beat the market,"Economist David McWilliams said Gen Z and millennials can outsmart a 'rigged' system by mastering money, interest rates, and smart investing.",https://www.businessinsider.com/ex-central-banker-shares-5-investing-rules-help-genz-build-wealth-2025-10,2025-10-10T10:25:44Z,"Ex—central banker David McWilliams says Gen Z can beat a rigged system by mastering money.Sheldon Cooper/SOPA Images/LightRocket via Getty Images
|
| 137 |
+
<ul><li>David McWilliams says Gen Z and millennials … [+4843 chars]"
|
| 138 |
+
Yahoo Entertainment,Nick Lichtenberg,"Top analyst still thinks we’re on the cusp of a new boom for the economy, but investors aren’t with him: ‘Markets remain choppy’",Morgan Stanley’s Mike Wilson says he needs to see more before giving the “all-clear” on chances of a further near-term correction.,https://finance.yahoo.com/news/top-analyst-still-thinks-cusp-171302613.html,2025-10-20T17:13:02Z,"Morgan Stanley chief equity analyst Mike Wilson has been saying for years the U.S. was in a rolling recession when economists were seeing nothing but GDP growth. Since April, hes been declaring a rol… [+5348 chars]"
|
| 139 |
+
Yahoo Entertainment,Jamie Stone,Top 5 Investments Boomers Should Make in Retirement — Even if It’s Begrudgingly,Even reluctant investors can benefit from these 5 smart retirement moves. Learn where boomers should put their money to stay secure and grow wealth.,https://finance.yahoo.com/news/top-5-investments-boomers-retirement-125507372.html,2025-10-03T12:55:07Z,"While baby boomers may prefer the comfort of traditional savings accounts and certificates of deposit (CDs), financial experts are urging retirees to embrace investments they might otherwise avoid.
|
| 140 |
+
… [+2256 chars]"
|
| 141 |
+
The Hill,"Jonathan Russo, opinion contributor","Opinion - Crypto world, buckle up for the rug-pull of all time","The crypto market is ripe for a rug pull, with insiders profiting from the inflated valuations of meme coins, crypto treasury stocks, and other scams...",https://thehill.com/opinion/finance/5548075-crypto-rug-pull-warning/,2025-10-10T16:00:00Z,"I have, like so many others, been wrong about Bitcoin and crypto. But timing is not destination, and I believe one day there will be a rug-pull that will wipe trillions off the face of the crypto ear… [+4766 chars]"
|
| 142 |
+
Yahoo Entertainment,Simply Wall St,Boeing (BA): Evaluating Valuation After Major Jet Orders and FAA Certification Boost,"If you’re watching Boeing’s stock this week and wondering whether it’s finally time to make a move, you’re not alone. The company just landed a trio of...",https://finance.yahoo.com/news/boeing-ba-evaluating-valuation-major-111522177.html,2025-09-28T11:15:22Z,"If youre watching Boeings stock this week and wondering whether its finally time to make a move, youre not alone. The company just landed a trio of headline-grabbing aircraft orders: Turkish Airlines… [+4595 chars]"
|
| 143 |
+
Barchart.com,Sushree Mohanty,This Under-the-Radar Data Center Stock Is Soaring Thanks to the AI Boom,"With demand accelerating, Arista’s AI future looks dynamic.",https://www.barchart.com/story/news/34960304/this-under-the-radar-data-center-stock-is-soaring-thanks-to-the-ai-boom,2025-09-22T17:43:43Z,"As the artificial intelligence (AI) wave grows into a tsunami, Arista Networks (ANET), an often-overlooked business in the data center ecosystem, is emerging as one of the big winners. Arista stock h… [+5157 chars]"
|
| 144 |
+
Yahoo Entertainment,Tirthankar Chakraborty,BBAI or SOUN: Which AI Stock Deserves a Spot in Your Portfolio?,"BigBear.ai's shrinking revenues and deepening losses highlight weak fundamentals, while SoundHound's surging sales and cash strength draw investor optimism.",https://finance.yahoo.com/news/bbai-soun-ai-stock-deserves-190000034.html,2025-10-14T19:00:00Z,"BigBear.ai Holdings, Inc. BBAI, which provides artificial intelligence (AI)-driven data analytics solutions to both the U.S. government and private-sector companies, saw its shares soar more than 400… [+4259 chars]"
|
| 145 |
+
Yahoo Entertainment,Alex Kimani,Trump’s Takeover Of Canadian Rare Earths Miners Raises Major Concerns,"Washington doubles down on critical minerals, with the Trump administration buying stakes in companies like Trilogy Metals, MP Materials, and Lithium...",https://finance.yahoo.com/news/trump-takeover-canadian-rare-earths-000000815.html,2025-10-14T00:00:00Z,"The U.S. stock market plunged by the widest margin in six months on Friday, with the S&P 500 shedding nearly 3% after President Donald Trump threatened “a massive increase of tariffs on Chinese p… [+5690 chars]"
|
| 146 |
+
Yahoo Entertainment,,The S&P 500 Is Poised to Do Something That's Only Happened 11 Times in 100 Years -- and It Could Signal a Big Move for the Stock Market in 2026,,https://consent.yahoo.com/v2/collectConsent?sessionId=1_cc-session_8f164d4e-c6e3-4732-a421-e2071d8c94d7,2025-10-14T08:44:00Z,"If you click 'Accept all', we and our partners, including 237 who are part of the IAB Transparency & Consent Framework, will also store and/or access information on a device (in other words, use … [+714 chars]"
|
| 147 |
+
Yahoo Entertainment,Chris Kirkham,Analysis-Musk's record Tesla package will pay him tens of billions even if he misses most goals,"LOS ANGELES (Reuters) -When Tesla directors offered Elon Musk the biggest executive pay package in corporate history in September, it reassured investors...",https://finance.yahoo.com/news/analysis-musks-record-tesla-package-100309588.html,2025-10-09T10:03:09Z,"By Chris Kirkham, Rachael Levy and Abhirup Roy
|
| 148 |
+
LOS ANGELES (Reuters) -When Tesla (TSLA) directors offered Elon Musk the biggest executive pay package in corporate history in September, it reassured … [+8412 chars]"
|
| 149 |
+
Yahoo Entertainment,Zacks Equity Research,"SoundHound AI, Inc. (SOUN) Exceeds Market Returns: Some Facts to Consider","SoundHound AI, Inc. (SOUN) closed the most recent trading day at $18.25, moving +2.24% from the previous trading session.",https://finance.yahoo.com/news/soundhound-ai-inc-soun-exceeds-214505837.html,2025-10-06T21:45:05Z,"In the latest trading session, SoundHound AI, Inc. (SOUN) closed at $18.25, marking a +2.24% move from the previous day. The stock exceeded the S&P 500, which registered a gain of 0.37% for the d… [+2435 chars]"
|
| 150 |
+
Yahoo Entertainment,Zacks Equity Research,AMD Gains Traction in AI Infrastructure Market: A Sign of More Upside?,"Advanced Micro Devices' growth is driven by AI footprint with record sales, new Instinct MI350 GPUs and major partnerships.",https://finance.yahoo.com/news/amd-gains-traction-ai-infrastructure-164000247.html,2025-09-25T16:40:00Z,"Advanced Micro Devices AMD is benefiting from strong traction in the AI infrastructure market, driven by its advanced product portfolio and strategic investments in AI hardware and software.
|
| 151 |
+
The co… [+3800 chars]"
|
| 152 |
+
Business Insider,Katie Notopoulos,"Nvidia plans to invest $100 billion into OpenAI. That's uh, a lot of money.",$100 billion is worth something like 333 AI researchers with Meta salaries.,https://www.businessinsider.com/nvidia-100-billion-openai-data-centers-scale-2025-9,2025-09-22T20:26:46Z,"Sam AltmanAndrew Harnik/Getty Images
|
| 153 |
+
<ul><li>Nvidia is investing huge amounts into OpenAI to help build more data centers.</li><li>They plan to build ""at least 10 gigawatts"" worth of data centers us… [+1621 chars]"
|
| 154 |
+
Business Insider,Alistair Barr,OpenAI's SaaS attack has begun. Here are the companies in the firing line.,"OpenAI showed off new AI tools for sales, support, and contracts, posing a direct threat to software-as-a-service leaders like Salesforce and HubSpot.",https://www.businessinsider.com/openai-saas-attack-hubspot-salesforce-docusign-zoominfo-2025-9,2025-09-30T23:47:08Z,"Sam Altman, CEO of OpenAI, meeting officials in Berlin.Florian Gaertner/Reuters
|
| 155 |
+
<ul><li>OpenAI showed off new workplace applications, entering the SaaS market as a competitor.</li><li>The move chall… [+3983 chars]"
|
| 156 |
+
CBS News,S.E. Jenkins,"SEC approves Texas Stock Exchange, first new US integrated exchange in decades","The SEC approved TXSE as a national securities exchange, paving the way for the first new, fully integrated U.S. stock exchange in decades — and the only one based in Texas.",https://www.cbsnews.com/texas/news/sec-approves-texas-stock-exchange-txse/,2025-10-04T16:04:24Z,"The Securities and Exchange Commission approved Tuesday the Texas Stock Exchange (TXSE) as a national securities exchange, paving the way for the first new, fully integrated U.S. stock exchange in de… [+2453 chars]"
|
| 157 |
+
Yahoo Entertainment,Simply Wall St,Top Dividend Stocks To Enhance Your Portfolio,"As the U.S. markets experience a strong start to the week, with major indices climbing over 1%, investors are keenly watching for corporate earnings and...",https://finance.yahoo.com/news/top-dividend-stocks-enhance-portfolio-113156039.html,2025-10-21T11:31:56Z,"As the U.S. markets experience a strong start to the week, with major indices climbing over 1%, investors are keenly watching for corporate earnings and economic data that could influence Federal Res… [+5439 chars]"
|
| 158 |
+
Quartz India,Catherine Baab,"Retail investors buy stocks in droves, fueling Wall Street bubble fears","Is this the bull market's final act? As mom-and-pop investors pile in, Wall Street observers hear echoes of past bubbles",https://qz.com/retail-investors-stock-market-wall-street,2025-10-10T12:20:45Z,"Retail investors are buying stocks in force and pushing up stock prices a dynamic thats spooking some veteran Wall Street analysts and observers.
|
| 159 |
+
According to data released this week , Citigroups in… [+3013 chars]"
|
| 160 |
+
Kotaku,Ethan Gach,The Steam Deck Just Went On Sale And Is Killing The Competition On Price,"Valve is shaving $80 off the PC gaming handheld for the fall Steam sale
|
| 161 |
+
The post The Steam Deck Just Went On Sale And Is Killing The Competition On Price appeared first on Kotaku.",https://kotaku.com/autumn-2025-steam-deck-sale-cheap-pc-gaming-handheld-2000627631,2025-09-22T18:51:52Z,"Despite a trade war and rising inflation, the three-year-old Steam Deck hasn’t gone up in price yet. In fact, it just got a pretty major discount. The $400 LCD model will be 20 percent off for the 20… [+1919 chars]"
|
| 162 |
+
Yahoo Entertainment,Arghyadeep Bose,Microvast Skyrockets 119% YTD: Is It a Must-Have Stock Now?,MVST's sharp rally and bold battery breakthroughs are turning heads. Can the momentum last?,https://finance.yahoo.com/news/microvast-skyrockets-119-ytd-must-160600141.html,2025-10-10T16:06:00Z,"Microvast Holdings MVST shares have experienced outstanding growth over the year-to-date period. It has skyrocketed 119.4% during the period, outperforming the 44.3% rise of its industry and the 15.7… [+4703 chars]"
|
| 163 |
+
Yahoo Entertainment,Simply Wall St,3 Reliable Dividend Stocks Offering Up To 7.1% Yield,"Amidst recent fluctuations in major stock indexes, with the S&P 500 and Nasdaq reaching new highs before pulling back, investors are increasingly seeking...",https://finance.yahoo.com/news/3-reliable-dividend-stocks-offering-173152475.html,2025-10-07T17:31:52Z,"Amidst recent fluctuations in major stock indexes, with the S&P 500 and Nasdaq reaching new highs before pulling back, investors are increasingly seeking stability through dividend stocks. In suc… [+5334 chars]"
|
| 164 |
+
TheStreet,Moz Farooque,IonQ CEO just threw a curveball at Nvidia,"In 2025, the quantum computing space essentially broke out of obscurity, with AI’s unrelenting demand spilling into virtually every compute bottleneck. For...",https://www.thestreet.com/technology/ionq-ceo-just-threw-a-curveball-at-nvidia,2025-10-15T02:33:00Z,"In 2025, the quantum computing space essentially broke out of obscurity, with AIs unrelenting demand spilling into virtually every compute bottleneck.
|
| 165 |
+
For perspective, the quantum cohort, as I like … [+4644 chars]"
|
| 166 |
+
Business Insider,Alex Nicoll,'Cockroach' jabs and regional bank breakdowns: The week private credit's 'golden' narrative got a little less shiny,"After years of hype, private credit had a rough week — as bank troubles and new warnings sparked a wave of scrutiny.",https://www.businessinsider.com/private-credit-bad-week-concerns-dimon-cockroach-comment-2025-10,2025-10-18T10:02:01Z,"Noam Galai/Getty Images/Carlo Allegri/Reuters/Jeff Swensen/Getty Images
|
| 167 |
+
<ul><li>Jamie Dimon's concern over ""cockroaches"" in credit has spawned a debate about private credit.</li><li>While some indus… [+7676 chars]"
|
| 168 |
+
Business Insider,Katherine Li,"Meet Lisa Su: CEO and president of Advanced Micro Devices, the main competitor to Nvidia","Lisa Su is the CEO behind AMD's meteoric rise, with the ambition to lead the AI chips industry and become a formidable rival to Nvidia.",https://www.businessinsider.com/meet-lisa-su-ceo-and-president-of-advanced-micro-device,2025-10-05T10:16:01Z,"Lisa Su is widely credited for accomplishing one of the most dramatic turnarounds in the tech industry, bringing AMD from a struggling company to an industry leader with a market cap of more than $27… [+10605 chars]"
|
| 169 |
+
Business Insider,jmann@insider.com (Jyoti Mann),Klarna chairman sent a stark post-IPO message to CEO: 'We're 10 years behind Revolut',Klarna CEO Sebastian Siemiatkowski kicked off its internal conference for employees last week with a rap performance.,https://www.businessinsider.com/klarna-ceo-chairman-ipo-message-behind-revolut-2025-9,2025-09-24T13:24:37Z,"At Klarna's big employee conference last week in Stockholm, CEO Sebastian Siemiatkowski swapped spreadsheets for a rap performance and then confronted a sobering reality. Even as Klarna basks in its … [+2318 chars]"
|
params.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tickers:
|
| 2 |
+
- AAPL
|
| 3 |
+
- GOOGL
|
| 4 |
+
- TSLA
|
| 5 |
+
|
| 6 |
+
sequence_length: 10
|
| 7 |
+
|
| 8 |
+
training:
|
| 9 |
+
test_split: 0.2
|
| 10 |
+
epochs: 50
|
| 11 |
+
batch_size: 32
|
| 12 |
+
|
| 13 |
+
sentiment:
|
| 14 |
+
pos_words: ["good", "buy", "up", "rise", "gain", "bull", "profit"]
|
| 15 |
+
neg_words: ["bad", "sell", "down", "fall", "loss", "bear", "risk"]
|
data/reddit_data.csv → reddit_data.csv
RENAMED
|
File without changes
|
requirements.txt
CHANGED
|
@@ -1,18 +1,31 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =====================
|
| 2 |
+
# Core scientific stack
|
| 3 |
+
# =====================
|
| 4 |
+
numpy==1.26.4
|
| 5 |
+
pandas==2.2.2
|
| 6 |
+
scipy==1.13.1
|
| 7 |
+
scikit-learn==1.4.2
|
| 8 |
+
|
| 9 |
+
# =====================
|
| 10 |
+
# MLOps & pipeline
|
| 11 |
+
# =====================
|
| 12 |
+
dvc==3.53.1
|
| 13 |
+
mlflow==2.14.3
|
| 14 |
+
river==0.21.0
|
| 15 |
+
|
| 16 |
+
# =====================
|
| 17 |
+
# Data sources
|
| 18 |
+
# =====================
|
| 19 |
+
yfinance==0.2.40
|
| 20 |
+
gnewsclient==1.12
|
| 21 |
+
joblib==1.4.2
|
| 22 |
+
|
| 23 |
+
# =====================
|
| 24 |
+
# Deep learning (CPU)
|
| 25 |
+
# =====================
|
| 26 |
+
torch==2.3.1
|
| 27 |
+
|
| 28 |
+
# =====================
|
| 29 |
+
# Utilities
|
| 30 |
+
# =====================
|
| 31 |
+
PyYAML==6.0.1
|
src/build_features.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/build_features.py
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
# ------------------------------------------------------------------
|
| 6 |
+
# Setup
|
| 7 |
+
# ------------------------------------------------------------------
|
| 8 |
+
os.makedirs("data/processed", exist_ok=True)
|
| 9 |
+
|
| 10 |
+
POS_WORDS = {"good", "buy", "up", "rise", "gain", "bull", "profit", "growth"}
|
| 11 |
+
NEG_WORDS = {"bad", "sell", "down", "fall", "loss", "bear", "risk", "crash"}
|
| 12 |
+
|
| 13 |
+
# ------------------------------------------------------------------
|
| 14 |
+
# Simple rule-based sentiment
|
| 15 |
+
# ------------------------------------------------------------------
|
| 16 |
+
def simple_sentiment(text):
|
| 17 |
+
if not isinstance(text, str):
|
| 18 |
+
return 0.0
|
| 19 |
+
words = text.lower().split()
|
| 20 |
+
pos = sum(w in POS_WORDS for w in words)
|
| 21 |
+
neg = sum(w in NEG_WORDS for w in words)
|
| 22 |
+
return (pos - neg) / (pos + neg) if (pos + neg) > 0 else 0.0
|
| 23 |
+
|
| 24 |
+
# ------------------------------------------------------------------
|
| 25 |
+
# Load & normalize news data
|
| 26 |
+
# ------------------------------------------------------------------
|
| 27 |
+
def load_news():
|
| 28 |
+
dfs = []
|
| 29 |
+
|
| 30 |
+
for fname in ["news_articles.csv", "gnews_data.csv", "reddit_data.csv"]:
|
| 31 |
+
path = f"data/raw/{fname}"
|
| 32 |
+
if os.path.exists(path):
|
| 33 |
+
df = pd.read_csv(path)
|
| 34 |
+
dfs.append(df)
|
| 35 |
+
|
| 36 |
+
if not dfs:
|
| 37 |
+
print("⚠ No news files found — sentiment will be zero")
|
| 38 |
+
return pd.DataFrame(columns=["date", "sentiment"])
|
| 39 |
+
|
| 40 |
+
news = pd.concat(dfs, ignore_index=True)
|
| 41 |
+
|
| 42 |
+
# Normalize text column
|
| 43 |
+
if "content" in news.columns:
|
| 44 |
+
news["text"] = news["content"]
|
| 45 |
+
elif "text" not in news.columns:
|
| 46 |
+
raise ValueError("No text/content column found in news data")
|
| 47 |
+
|
| 48 |
+
# Normalize datetime column
|
| 49 |
+
if "publishedAt" not in news.columns:
|
| 50 |
+
raise ValueError("No publishedAt column found in news data")
|
| 51 |
+
|
| 52 |
+
news["publishedAt"] = pd.to_datetime(news["publishedAt"], errors="coerce")
|
| 53 |
+
news = news.dropna(subset=["publishedAt"])
|
| 54 |
+
|
| 55 |
+
news["date"] = news["publishedAt"].dt.date
|
| 56 |
+
news["sentiment"] = news["text"].apply(simple_sentiment)
|
| 57 |
+
|
| 58 |
+
# Daily aggregated sentiment
|
| 59 |
+
daily_sent = (
|
| 60 |
+
news.groupby("date")["sentiment"]
|
| 61 |
+
.mean()
|
| 62 |
+
.reset_index()
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
return daily_sent
|
| 66 |
+
|
| 67 |
+
# ------------------------------------------------------------------
|
| 68 |
+
# Main feature pipeline
|
| 69 |
+
# ------------------------------------------------------------------
|
| 70 |
+
def main():
|
| 71 |
+
# -------------------------------
|
| 72 |
+
# Load stock prices
|
| 73 |
+
# -------------------------------
|
| 74 |
+
prices = pd.read_csv("data/raw/stock_prices.csv")
|
| 75 |
+
prices = prices.dropna(subset=["Ticker"])
|
| 76 |
+
|
| 77 |
+
prices["Date"] = pd.to_datetime(prices["Date"], utc=True)
|
| 78 |
+
prices["date"] = prices["Date"].dt.date
|
| 79 |
+
|
| 80 |
+
# Ensure numeric columns (CRITICAL FIX)
|
| 81 |
+
for col in ["Close", "Volume", "Return"]:
|
| 82 |
+
if col in prices.columns:
|
| 83 |
+
prices[col] = pd.to_numeric(prices[col], errors="coerce")
|
| 84 |
+
|
| 85 |
+
# -------------------------------
|
| 86 |
+
# Load sentiment
|
| 87 |
+
# -------------------------------
|
| 88 |
+
daily_sent = load_news()
|
| 89 |
+
|
| 90 |
+
# -------------------------------
|
| 91 |
+
# Merge prices + sentiment
|
| 92 |
+
# -------------------------------
|
| 93 |
+
merged = prices.merge(daily_sent, on="date", how="left")
|
| 94 |
+
merged["sentiment"] = merged["sentiment"].fillna(0)
|
| 95 |
+
|
| 96 |
+
merged = merged.sort_values(["Ticker", "Date"])
|
| 97 |
+
|
| 98 |
+
# -------------------------------
|
| 99 |
+
# Lag features
|
| 100 |
+
# -------------------------------
|
| 101 |
+
merged["return_lag1"] = merged.groupby("Ticker")["Return"].shift(1)
|
| 102 |
+
merged["volume_lag1"] = merged.groupby("Ticker")["Volume"].shift(1)
|
| 103 |
+
merged["sentiment_lag1"] = merged.groupby("Ticker")["sentiment"].shift(1)
|
| 104 |
+
|
| 105 |
+
# -------------------------------
|
| 106 |
+
# Coerce lagged columns to numeric
|
| 107 |
+
# -------------------------------
|
| 108 |
+
merged["return_lag1"] = pd.to_numeric(
|
| 109 |
+
merged["return_lag1"], errors="coerce"
|
| 110 |
+
).fillna(0)
|
| 111 |
+
|
| 112 |
+
merged["volume_lag1"] = pd.to_numeric(
|
| 113 |
+
merged["volume_lag1"], errors="coerce"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# Compute per-ticker median lagged volume
|
| 117 |
+
median_volume = merged.groupby("Ticker")["volume_lag1"].median()
|
| 118 |
+
|
| 119 |
+
# Map median volume back to rows (vectorized, NaN-safe)
|
| 120 |
+
merged["volume_lag1"] = merged["volume_lag1"].fillna(
|
| 121 |
+
merged["Ticker"].map(median_volume)
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Final fallback if still NaN (e.g., ticker itself missing)
|
| 125 |
+
merged["volume_lag1"] = merged["volume_lag1"].fillna(0)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
merged["sentiment_lag1"] = merged["sentiment_lag1"].fillna(0)
|
| 129 |
+
|
| 130 |
+
# -------------------------------
|
| 131 |
+
# Final sanity filter
|
| 132 |
+
# -------------------------------
|
| 133 |
+
merged = merged[merged["Ticker"].notna()]
|
| 134 |
+
|
| 135 |
+
# -------------------------------
|
| 136 |
+
# Save output
|
| 137 |
+
# -------------------------------
|
| 138 |
+
merged.to_csv("data/processed/merged_features.csv", index=False)
|
| 139 |
+
|
| 140 |
+
print("Saved data/processed/merged_features.csv")
|
| 141 |
+
print("Rows:", len(merged))
|
| 142 |
+
print("Tickers:", merged["Ticker"].unique())
|
| 143 |
+
|
| 144 |
+
# ------------------------------------------------------------------
|
| 145 |
+
if __name__ == "__main__":
|
| 146 |
+
main()
|
src/drift_detection.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/drift_detection.py
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from datetime import timedelta
|
| 6 |
+
from river.drift import ADWIN, PageHinkley
|
| 7 |
+
|
| 8 |
+
DATA_PATH = "data/processed/merged_features.csv"
|
| 9 |
+
OUT_DIR = "drift_reports"
|
| 10 |
+
os.makedirs(OUT_DIR, exist_ok=True)
|
| 11 |
+
|
| 12 |
+
FEATURES = ["return_lag1", "volume_lag1", "sentiment_lag1"]
|
| 13 |
+
CURRENT_DAYS = 30
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
df = pd.read_csv(DATA_PATH)
|
| 17 |
+
df["Date"] = pd.to_datetime(df["Date"])
|
| 18 |
+
|
| 19 |
+
cutoff = df["Date"].max() - timedelta(days=CURRENT_DAYS)
|
| 20 |
+
recent = df[df["Date"] >= cutoff]
|
| 21 |
+
|
| 22 |
+
if recent.empty:
|
| 23 |
+
raise RuntimeError("No recent data for drift detection")
|
| 24 |
+
|
| 25 |
+
results = {}
|
| 26 |
+
|
| 27 |
+
for f in FEATURES:
|
| 28 |
+
adwin = ADWIN()
|
| 29 |
+
ph = PageHinkley()
|
| 30 |
+
|
| 31 |
+
drift_points = []
|
| 32 |
+
|
| 33 |
+
for val in recent[f].dropna():
|
| 34 |
+
adwin.update(val)
|
| 35 |
+
ph.update(val)
|
| 36 |
+
|
| 37 |
+
if adwin.drift_detected or ph.drift_detected:
|
| 38 |
+
drift_points.append(True)
|
| 39 |
+
else:
|
| 40 |
+
drift_points.append(False)
|
| 41 |
+
|
| 42 |
+
results[f] = {
|
| 43 |
+
"adwin_drift": adwin.drift_detected,
|
| 44 |
+
"page_hinkley_drift": ph.drift_detected,
|
| 45 |
+
"drift_flag": adwin.drift_detected or ph.drift_detected
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
out_path = os.path.join(OUT_DIR, "drift_summary.json")
|
| 49 |
+
with open(out_path, "w") as f:
|
| 50 |
+
json.dump(results, f, indent=4)
|
| 51 |
+
|
| 52 |
+
print("River drift detection completed")
|
| 53 |
+
print(json.dumps(results, indent=2))
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
main()
|
src/evaluate_models.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/evaluate_models.py
|
| 2 |
+
import json
|
| 3 |
+
import joblib
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from sklearn.metrics import mean_squared_error, mean_absolute_error
|
| 7 |
+
|
| 8 |
+
def main():
|
| 9 |
+
df = pd.read_csv("data/processed/merged_features.csv")
|
| 10 |
+
metrics = {}
|
| 11 |
+
|
| 12 |
+
for t in df["Ticker"].unique():
|
| 13 |
+
mdir = f"models/{t}"
|
| 14 |
+
rf = joblib.load(f"{mdir}/rf.joblib")
|
| 15 |
+
sx = joblib.load(f"{mdir}/scaler_x.joblib")
|
| 16 |
+
sy = joblib.load(f"{mdir}/scaler_y.joblib")
|
| 17 |
+
|
| 18 |
+
df_t = df[df["Ticker"] == t].copy()
|
| 19 |
+
X = df_t[["return_lag1","volume_lag1","sentiment_lag1"]].values
|
| 20 |
+
y = df_t["Return"].shift(-1).dropna().values
|
| 21 |
+
X = X[:-1]
|
| 22 |
+
|
| 23 |
+
Xs = sx.transform(X)
|
| 24 |
+
preds = sy.inverse_transform(rf.predict(Xs).reshape(-1,1)).flatten()
|
| 25 |
+
|
| 26 |
+
metrics[t] = {
|
| 27 |
+
"RMSE": float(np.sqrt(mean_squared_error(y, preds))),
|
| 28 |
+
"MAE": float(mean_absolute_error(y, preds))
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
with open("metrics/evaluation.json", "w") as f:
|
| 32 |
+
json.dump(metrics, f, indent=2)
|
| 33 |
+
|
| 34 |
+
print("Saved evaluation.json")
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
main()
|
src/ingest_data.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/ingest_data.py
|
| 2 |
+
import os
|
| 3 |
+
import shutil
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import yfinance as yf
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
RAW_DIR = "data/raw"
|
| 9 |
+
os.makedirs(RAW_DIR, exist_ok=True)
|
| 10 |
+
|
| 11 |
+
TICKERS = ["AAPL", "GOOGL", "TSLA"]
|
| 12 |
+
START_DATE = "2015-01-01"
|
| 13 |
+
END_DATE = datetime.now().strftime("%Y-%m-%d")
|
| 14 |
+
|
| 15 |
+
def fetch_stock_data():
|
| 16 |
+
frames = []
|
| 17 |
+
for t in TICKERS:
|
| 18 |
+
df = yf.download(t, start=START_DATE, end=END_DATE, progress=False)
|
| 19 |
+
if df.empty:
|
| 20 |
+
continue
|
| 21 |
+
df.reset_index(inplace=True)
|
| 22 |
+
df["Ticker"] = t
|
| 23 |
+
df["Return"] = df["Close"].pct_change()
|
| 24 |
+
frames.append(df)
|
| 25 |
+
|
| 26 |
+
out = pd.concat(frames, ignore_index=True)
|
| 27 |
+
out.to_csv(f"{RAW_DIR}/stock_prices.csv", index=False)
|
| 28 |
+
print("Saved stock_prices.csv")
|
| 29 |
+
|
| 30 |
+
def copy_news_files():
|
| 31 |
+
"""
|
| 32 |
+
Move news data from data/ → data/raw/ so DVC owns it
|
| 33 |
+
"""
|
| 34 |
+
source_dir = "data"
|
| 35 |
+
target_dir = "data/raw"
|
| 36 |
+
os.makedirs(target_dir, exist_ok=True)
|
| 37 |
+
|
| 38 |
+
source_files = [
|
| 39 |
+
"news_articles.csv",
|
| 40 |
+
"gnews_data.csv",
|
| 41 |
+
"reddit_data.csv"
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
for f in source_files:
|
| 45 |
+
src = os.path.join(source_dir, f)
|
| 46 |
+
dst = os.path.join(target_dir, f)
|
| 47 |
+
|
| 48 |
+
if not os.path.exists(src):
|
| 49 |
+
print(f"Warning: {src} not found")
|
| 50 |
+
continue
|
| 51 |
+
|
| 52 |
+
if os.path.abspath(src) == os.path.abspath(dst):
|
| 53 |
+
print(f"Skipping {f} (already in target location)")
|
| 54 |
+
continue
|
| 55 |
+
|
| 56 |
+
shutil.copy(src, dst)
|
| 57 |
+
print(f"Copied {src} → {dst}")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
fetch_stock_data()
|
| 62 |
+
copy_news_files()
|
src/live_service.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/live_service.py
|
| 2 |
+
import time
|
| 3 |
+
import joblib
|
| 4 |
+
import numpy as np
|
| 5 |
+
import yfinance as yf
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
|
| 9 |
+
class MLP(nn.Module):
|
| 10 |
+
def __init__(self, n):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.net = nn.Sequential(nn.Linear(n,32), nn.ReLU(), nn.Linear(32,1))
|
| 13 |
+
def forward(self, x): return self.net(x)
|
| 14 |
+
|
| 15 |
+
def load_models(ticker):
|
| 16 |
+
base = f"models/{ticker}"
|
| 17 |
+
rf = joblib.load(f"{base}/rf.joblib")
|
| 18 |
+
sx = joblib.load(f"{base}/scaler_x.joblib")
|
| 19 |
+
sy = joblib.load(f"{base}/scaler_y.joblib")
|
| 20 |
+
|
| 21 |
+
mlp = MLP(3)
|
| 22 |
+
mlp.load_state_dict(torch.load(f"{base}/mlp.pth"))
|
| 23 |
+
mlp.eval()
|
| 24 |
+
|
| 25 |
+
return rf, mlp, sx, sy
|
| 26 |
+
|
| 27 |
+
MODELS = {t: load_models(t) for t in ["AAPL","GOOGL","TSLA"]}
|
| 28 |
+
|
| 29 |
+
while True:
|
| 30 |
+
for t, (rf, mlp, sx, sy) in MODELS.items():
|
| 31 |
+
df = yf.download(t, period="1d", interval="1m", progress=False)
|
| 32 |
+
if df.empty: continue
|
| 33 |
+
last = df.iloc[-1]
|
| 34 |
+
X = np.array([[0, last["Volume"], 0]])
|
| 35 |
+
Xs = sx.transform(X)
|
| 36 |
+
|
| 37 |
+
pred_rf = sy.inverse_transform(rf.predict(Xs).reshape(-1,1))[0][0]
|
| 38 |
+
pred_mlp = sy.inverse_transform(
|
| 39 |
+
mlp(torch.tensor(Xs, dtype=torch.float32)).detach().numpy()
|
| 40 |
+
)[0][0]
|
| 41 |
+
|
| 42 |
+
print(f"{t} → RF:{pred_rf:.6f} MLP:{pred_mlp:.6f}")
|
| 43 |
+
|
| 44 |
+
time.sleep(60)
|
src/retrain_if_drift.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/retrain_if_drift.py
|
| 2 |
+
import json
|
| 3 |
+
import subprocess
|
| 4 |
+
|
| 5 |
+
DECISION_FILE = "drift_reports/retrain_flag.json"
|
| 6 |
+
|
| 7 |
+
with open(DECISION_FILE) as f:
|
| 8 |
+
decision = json.load(f)
|
| 9 |
+
|
| 10 |
+
if decision.get("retrain", False):
|
| 11 |
+
print("Retraining triggered")
|
| 12 |
+
subprocess.run(["python", "src/train_models.py"], check=True)
|
| 13 |
+
else:
|
| 14 |
+
print("Retraining skipped (no drift)")
|
src/should_retrain.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/should_retrain.py
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
DRIFT_FILE = "drift_reports/drift_summary.json"
|
| 6 |
+
DECISION_FILE = "drift_reports/retrain_flag.json"
|
| 7 |
+
|
| 8 |
+
def main():
|
| 9 |
+
with open(DRIFT_FILE) as f:
|
| 10 |
+
drift = json.load(f)
|
| 11 |
+
|
| 12 |
+
retrain = any(
|
| 13 |
+
v.get("drift_flag", False) for v in drift.values()
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
decision = {"retrain": retrain}
|
| 17 |
+
|
| 18 |
+
with open(DECISION_FILE, "w") as f:
|
| 19 |
+
json.dump(decision, f, indent=4)
|
| 20 |
+
|
| 21 |
+
if retrain:
|
| 22 |
+
print("Drift detected → retraining required")
|
| 23 |
+
else:
|
| 24 |
+
print("No drift detected → retraining not required")
|
| 25 |
+
|
| 26 |
+
if __name__ == "__main__":
|
| 27 |
+
main()
|
src/train_models.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/train_models.py
|
| 2 |
+
import os
|
| 3 |
+
import joblib
|
| 4 |
+
import mlflow
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 11 |
+
from sklearn.ensemble import RandomForestRegressor
|
| 12 |
+
from sklearn.metrics import mean_squared_error
|
| 13 |
+
|
| 14 |
+
# ------------------------------------------------------------------
|
| 15 |
+
# MLflow setup
|
| 16 |
+
# ------------------------------------------------------------------
|
| 17 |
+
mlflow.set_tracking_uri("sqlite:///mlflow.db")
|
| 18 |
+
mlflow.set_experiment("Investor-Sentiment-Aware-Models")
|
| 19 |
+
|
| 20 |
+
# ------------------------------------------------------------------
|
| 21 |
+
# Ensure models directory exists
|
| 22 |
+
# ------------------------------------------------------------------
|
| 23 |
+
os.makedirs("models", exist_ok=True)
|
| 24 |
+
|
| 25 |
+
# ------------------------------------------------------------------
|
| 26 |
+
# Simple MLP model
|
| 27 |
+
# ------------------------------------------------------------------
|
| 28 |
+
class MLP(nn.Module):
|
| 29 |
+
def __init__(self, n_features):
|
| 30 |
+
super().__init__()
|
| 31 |
+
self.net = nn.Sequential(
|
| 32 |
+
nn.Linear(n_features, 32),
|
| 33 |
+
nn.ReLU(),
|
| 34 |
+
nn.Linear(32, 1)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
def forward(self, x):
|
| 38 |
+
return self.net(x)
|
| 39 |
+
|
| 40 |
+
# ------------------------------------------------------------------
|
| 41 |
+
# Train models for a single ticker
|
| 42 |
+
# ------------------------------------------------------------------
|
| 43 |
+
def train_ticker(df, ticker):
|
| 44 |
+
df_t = df[df["Ticker"] == ticker].copy()
|
| 45 |
+
|
| 46 |
+
# Feature matrix
|
| 47 |
+
X = df_t[["return_lag1", "volume_lag1", "sentiment_lag1"]].values
|
| 48 |
+
y = df_t["Return"].shift(-1).dropna().values
|
| 49 |
+
|
| 50 |
+
# Align X with shifted y
|
| 51 |
+
X = X[:-1]
|
| 52 |
+
|
| 53 |
+
if len(X) < 20:
|
| 54 |
+
raise ValueError(f"Not enough samples after lagging for {ticker}")
|
| 55 |
+
|
| 56 |
+
# Scale
|
| 57 |
+
sx, sy = MinMaxScaler(), MinMaxScaler()
|
| 58 |
+
Xs = sx.fit_transform(X)
|
| 59 |
+
ys = sy.fit_transform(y.reshape(-1, 1)).flatten()
|
| 60 |
+
|
| 61 |
+
split = int(0.8 * len(Xs))
|
| 62 |
+
Xtr, Xte = Xs[:split], Xs[split:]
|
| 63 |
+
ytr, yte = ys[:split], ys[split:]
|
| 64 |
+
|
| 65 |
+
ticker_dir = f"models/{ticker}"
|
| 66 |
+
os.makedirs(ticker_dir, exist_ok=True)
|
| 67 |
+
|
| 68 |
+
with mlflow.start_run(run_name=ticker):
|
| 69 |
+
mlflow.log_param("ticker", ticker)
|
| 70 |
+
mlflow.log_param("train_samples", len(Xtr))
|
| 71 |
+
mlflow.log_param("test_samples", len(Xte))
|
| 72 |
+
|
| 73 |
+
# -------------------------------
|
| 74 |
+
# Random Forest
|
| 75 |
+
# -------------------------------
|
| 76 |
+
rf = RandomForestRegressor(
|
| 77 |
+
n_estimators=200,
|
| 78 |
+
random_state=42
|
| 79 |
+
)
|
| 80 |
+
rf.fit(Xtr, ytr)
|
| 81 |
+
|
| 82 |
+
preds_rf = rf.predict(Xte)
|
| 83 |
+
rmse_rf = np.sqrt(mean_squared_error(yte, preds_rf))
|
| 84 |
+
|
| 85 |
+
joblib.dump(rf, f"{ticker_dir}/rf.joblib")
|
| 86 |
+
mlflow.sklearn.log_model(rf, "rf")
|
| 87 |
+
mlflow.log_metric("rf_rmse", rmse_rf)
|
| 88 |
+
|
| 89 |
+
# -------------------------------
|
| 90 |
+
# MLP
|
| 91 |
+
# -------------------------------
|
| 92 |
+
mlp = MLP(X.shape[1])
|
| 93 |
+
optimizer = torch.optim.Adam(mlp.parameters(), lr=0.001)
|
| 94 |
+
loss_fn = nn.MSELoss()
|
| 95 |
+
|
| 96 |
+
Xtr_t = torch.tensor(Xtr, dtype=torch.float32)
|
| 97 |
+
ytr_t = torch.tensor(ytr, dtype=torch.float32).unsqueeze(1)
|
| 98 |
+
|
| 99 |
+
for epoch in range(50):
|
| 100 |
+
optimizer.zero_grad()
|
| 101 |
+
loss = loss_fn(mlp(Xtr_t), ytr_t)
|
| 102 |
+
loss.backward()
|
| 103 |
+
optimizer.step()
|
| 104 |
+
|
| 105 |
+
mlp.eval()
|
| 106 |
+
Xte_t = torch.tensor(Xte, dtype=torch.float32)
|
| 107 |
+
preds_mlp = mlp(Xte_t).detach().numpy().flatten()
|
| 108 |
+
rmse_mlp = np.sqrt(mean_squared_error(yte, preds_mlp))
|
| 109 |
+
|
| 110 |
+
torch.save(mlp.state_dict(), f"{ticker_dir}/mlp.pth")
|
| 111 |
+
mlflow.pytorch.log_model(mlp, "mlp")
|
| 112 |
+
mlflow.log_metric("mlp_rmse", rmse_mlp)
|
| 113 |
+
|
| 114 |
+
# -------------------------------
|
| 115 |
+
# Scalers
|
| 116 |
+
# -------------------------------
|
| 117 |
+
joblib.dump(sx, f"{ticker_dir}/scaler_x.joblib")
|
| 118 |
+
joblib.dump(sy, f"{ticker_dir}/scaler_y.joblib")
|
| 119 |
+
|
| 120 |
+
print(
|
| 121 |
+
f"[{ticker}] RF RMSE={rmse_rf:.6f}, "
|
| 122 |
+
f"MLP RMSE={rmse_mlp:.6f}"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# ------------------------------------------------------------------
|
| 126 |
+
# Main entry point (DVC stage)
|
| 127 |
+
# ------------------------------------------------------------------
|
| 128 |
+
def main():
|
| 129 |
+
df = pd.read_csv("data/processed/merged_features.csv")
|
| 130 |
+
|
| 131 |
+
print("Rows in merged features:", len(df))
|
| 132 |
+
print("Tickers found:", df["Ticker"].unique())
|
| 133 |
+
|
| 134 |
+
trained_any = False
|
| 135 |
+
|
| 136 |
+
for ticker in df["Ticker"].unique():
|
| 137 |
+
df_t = df[df["Ticker"] == ticker]
|
| 138 |
+
|
| 139 |
+
if len(df_t) < 50:
|
| 140 |
+
print(f"Skipping {ticker}: insufficient data ({len(df_t)} rows)")
|
| 141 |
+
continue
|
| 142 |
+
|
| 143 |
+
print(f"Training models for {ticker}")
|
| 144 |
+
train_ticker(df, ticker)
|
| 145 |
+
trained_any = True
|
| 146 |
+
|
| 147 |
+
if not trained_any:
|
| 148 |
+
raise RuntimeError(
|
| 149 |
+
"No models were trained — check feature generation or data volume."
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
print("Training stage completed successfully.")
|
| 153 |
+
|
| 154 |
+
# ------------------------------------------------------------------
|
| 155 |
+
if __name__ == "__main__":
|
| 156 |
+
main()
|