diff --git a/core/__pycache__/market_utils.cpython-311.pyc b/core/__pycache__/market_utils.cpython-311.pyc new file mode 100644 index 0000000..a3c9c56 Binary files /dev/null and b/core/__pycache__/market_utils.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ebb8c6e..0a0486d 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 8d204fa..f57bd5e 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/market_utils.py b/core/market_utils.py new file mode 100644 index 0000000..9d57def --- /dev/null +++ b/core/market_utils.py @@ -0,0 +1,204 @@ +import yfinance as yf +import pandas as pd +import numpy as np +from textblob import TextBlob +import feedparser +import httpx +from typing import List, Tuple, Dict, Any +import warnings + +# Suppress yfinance multi-index warning for cleaner logs +warnings.filterwarnings("ignore", category=FutureWarning) + +class MarkovChain: + def __init__(self, symbol: str, bins: int = 5): + self.symbol = symbol + self.bins = bins + + def fetch_data(self) -> pd.DataFrame: + try: + # Using period="1y" and auto_adjust=True + data = yf.download(self.symbol, period="1y", interval="1d", progress=False) + if data.empty: + return None + data["returns"] = data["Close"].pct_change() + return data.dropna() + except Exception: + return None + + def build_states(self, data: pd.DataFrame) -> pd.DataFrame: + data["state"] = pd.qcut( + data["returns"], + self.bins, + labels=False, + duplicates="drop" + ) + return data + + def prob_matrix(self) -> Tuple[np.ndarray, int]: + data = self.fetch_data() + if data is None: + return None, None + + data = self.build_states(data) + states = data["state"].astype(int).values + + unique_states = np.unique(states) + bins = len(unique_states) + transition_matrix = np.zeros((bins, bins)) + + for i in range(len(states) - 1): + current_state = states[i] + next_state = states[i + 1] + transition_matrix[current_state][next_state] += 1 + + row_sums = transition_matrix.sum(axis=1, keepdims=True) + # Fix the np.divide warning by providing 'out' and making it single-line/cleaner + transition_matrix = np.divide( + transition_matrix, + row_sums, + out=np.zeros_like(transition_matrix), + where=row_sums != 0 + ) + + recent_state = int(states[-1]) + return transition_matrix, recent_state + +def add_moving_averages(data: pd.DataFrame, fast: int = 20, slow: int = 50) -> pd.DataFrame: + data["SMA_fast"] = data["Close"].rolling(window=fast).mean() + data["SMA_slow"] = data["Close"].rolling(window=slow).mean() + return data + +def add_crossover_signals(data: pd.DataFrame) -> pd.DataFrame: + data["signal"] = 0 + data.loc[data["SMA_fast"] > data["SMA_slow"], "signal"] = 1 + data["position_change"] = data["signal"].diff() + + data["event"] = "" + data.loc[data["position_change"] == 1, "event"] = "Bullish Crossover" + data.loc[data["position_change"] == -1, "event"] = "Bearish Crossover" + return data + +def fetch_news() -> List[str]: + feeds = [ + "https://feeds.bbci.co.uk/news/business/rss.xml", + "https://feeds.reuters.com/reuters/businessNews", + ] + headlines = [] + for url in feeds: + try: + feed = feedparser.parse(url) + for entry in feed.entries[:10]: # reduced to 10 per feed for speed + headlines.append(entry.title) + except Exception: + continue + return list(set(headlines)) + +def analyze_sentiment(headlines: List[str]) -> List[Tuple[str, float, str]]: + results = [] + for h in headlines: + polarity = TextBlob(h).sentiment.polarity + if polarity > 0: + label = "Positive" + elif polarity < 0: + label = "Negative" + else: + label = "Neutral" + results.append((h, polarity, label)) + return sorted(results, key=lambda x: x[1], reverse=True) + +def get_market_analysis(symbol: str) -> Dict[str, Any]: + # Markov Analysis + mc = MarkovChain(symbol) + matrix, recent_state = mc.prob_matrix() + markov_data = None + if matrix is not None: + next_probs = matrix[recent_state] + predicted_state = int(np.argmax(next_probs)) + probability = float(next_probs[predicted_state]) + + if predicted_state > recent_state: + bias = "Bullish" + color = "success" + elif predicted_state < recent_state: + bias = "Bearish" + color = "danger" + else: + bias = "Neutral" + color = "secondary" + + markov_data = { + "current_state": recent_state, + "predicted_state": predicted_state, + "probability": f"{probability:.2%}", + "bias": bias, + "color": color, + "state_labels": [ + {"id": 0, "label": "Strong Bearish", "color": "vibrant-red"}, + {"id": 1, "label": "Bearish", "color": "text-muted"}, + {"id": 2, "label": "Neutral", "color": "text-light"}, + {"id": 3, "label": "Bullish", "color": "cyber-blue"}, + {"id": 4, "label": "Strong Bullish", "color": "neon-green"} + ] + } + + # Technical Analysis + data = yf.download(symbol, period="6mo", interval="1d", progress=False) + tech_data = None + if not data.empty: + data = add_moving_averages(data) + data = add_crossover_signals(data) + latest = data.iloc[-1] + + # In newer yfinance versions, 'Close' can be a series if it's a MultiIndex (even with 1 symbol) + close_val = float(latest['Close'].iloc[0]) if hasattr(latest['Close'], 'iloc') else float(latest['Close']) + sma_fast = float(latest['SMA_fast'].iloc[0]) if hasattr(latest['SMA_fast'], 'iloc') else float(latest['SMA_fast']) + sma_slow = float(latest['SMA_slow'].iloc[0]) if hasattr(latest['SMA_slow'], 'iloc') else float(latest['SMA_slow']) + + if sma_fast > sma_slow: + trend = "Bullish" + color = "success" + else: + trend = "Bearish" + color = "danger" + + last_event = data[data["event"] != ""] + last_event_msg = last_event.iloc[-1]["event"] if not last_event.empty else "No recent crossover" + + tech_data = { + "latest_close": f"${close_val:.2f}", + "sma_20": f"${sma_fast:.2f}", + "sma_50": f"${sma_slow:.2f}", + "trend": trend, + "color": color, + "last_event": last_event_msg + } + + # News Sentiment + headlines = fetch_news() + sentiment_results = analyze_sentiment(headlines) + avg_sentiment = float(np.mean([x[1] for x in sentiment_results])) if sentiment_results else 0.0 + + if avg_sentiment > 0: + overall = "Positive" + color = "success" + elif avg_sentiment < 0: + overall = "Negative" + color = "danger" + else: + overall = "Neutral" + color = "secondary" + + sentiment_data = { + "avg_sentiment": f"{avg_sentiment:.2f}", + "overall": overall, + "color": color, + "top_headlines": [{"title": h, "label": l, "polarity": f"{p:.2f}"} for h, p, l in sentiment_results[:5]] + } + + return { + "symbol": symbol.upper(), + "markov": markov_data, + "tech": tech_data, + "sentiment": sentiment_data + } \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..4e7b035 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,57 @@ -
- -