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 @@ - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}Market Intelligence Dashboard{% endblock %} + + + + + + + + + + {% load static %} + + + {% block extra_head %}{% endblock %} - - {% block content %}{% endblock %} - + - +
+ {% block content %}{% endblock %} +
+ + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..e897703 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,157 @@ -{% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% extends 'base.html' %} +{% load static %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+
+

📊 Market Intelligence

+

Markov Chains • Moving Averages • News Sentiment

+
+ +
+
+
+ + +
+
+
+ + {% if error %} + + {% endif %} + + {% if analysis %} +
+ +
+
+

Markov Forecast

+ {% if analysis.markov %} +
+
Current State
+
{{ analysis.markov.current_state }}
+
+
+
Predicted State
+
{{ analysis.markov.predicted_state }}
+
+
+
Probability
+
{{ analysis.markov.probability }}
+
+
+
+ Market Bias: + {{ analysis.markov.bias }} +
+ + +
+
State Key:
+
+ {% for state in analysis.markov.state_labels %} +
+ {{ state.id }}: + {{ state.label }} +
+ {% endfor %} +
+
+
+ {% else %} +

Insufficient data for Markov analysis.

+ {% endif %} +
+
+ + +
+
+

Technical Trend

+ {% if analysis.tech %} +
+
Latest Close
+
{{ analysis.tech.latest_close }}
+
+
+
+
SMA 20
+
{{ analysis.tech.sma_20 }}
+
+
+
SMA 50
+
{{ analysis.tech.sma_50 }}
+
+
+
+
Last Crossover
+
{{ analysis.tech.last_event }}
+
+
+
+ Trend: + {{ analysis.tech.trend }} +
+
+ {% else %} +

Unable to fetch price data.

+ {% endif %} +
+
+ + +
+
+

News Sentiment

+ {% if analysis.sentiment %} +
+
Average Sentiment
+
{{ analysis.sentiment.avg_sentiment }}
+
+ Overall Bias: + {{ analysis.sentiment.overall }} +
+
+
+
Top Headlines
+ {% for headline in analysis.sentiment.top_headlines %} +
+
+ {{ headline.title }} + {{ headline.polarity }} +
+
+ {% endfor %} +
+ {% else %} +

Unable to fetch news sentiment.

+ {% endif %} +
+
+
+ {% endif %}
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- + + + {% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..c486297 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,6 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('', views.index, name='index'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..ce9eb1b 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,24 @@ -import os -import platform - -from django import get_version as django_version from django.shortcuts import render -from django.utils import timezone +from .market_utils import get_market_analysis +def index(request): + symbol = request.GET.get("symbol", "AAPL").strip() + analysis = None + error = None -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + if symbol: + try: + analysis = get_market_analysis(symbol) + if not analysis.get("markov") and not analysis.get("tech"): + error = f"Unable to fetch data for symbol: {symbol}" + analysis = None + except Exception as e: + error = f"An error occurred: {str(e)}" + analysis = None context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "analysis": analysis, + "error": error, + "symbol": symbol } - return render(request, "core/index.html", context) + return render(request, "core/index.html", context) \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..f8b22cb 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,115 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +:root { + --bg-dark: #121212; + --glass-bg: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.1); + --cyber-blue: #00d2ff; + --neon-green: #39ff14; + --vibrant-red: #ff3131; + --text-muted: #888; + --text-light: #e0e0e0; } + +body { + background-color: var(--bg-dark); + color: var(--text-light); + font-family: 'Inter', sans-serif; + margin: 0; + padding: 0; +} + +h1, h2, h3, h4, .brand-font { + font-family: 'Montserrat', sans-serif; + font-weight: 700; +} + +.glass-card { + background: var(--glass-bg); + backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: 16px; + padding: 24px; + height: 100%; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.glass-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + border-color: var(--cyber-blue); +} + +.metric-value { + font-size: 2.5rem; + font-weight: 800; + margin: 10px 0; + background: linear-gradient(45deg, var(--cyber-blue), #fff); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.metric-label { + color: var(--text-muted); + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +.ticker-input { + background: rgba(255, 255, 255, 0.1); + border: 2px solid var(--glass-border); + border-radius: 12px; + color: #fff; + padding: 12px 20px; + font-size: 1.2rem; + width: 300px; + transition: border-color 0.3s ease; +} + +.ticker-input:focus { + outline: none; + border-color: var(--cyber-blue); + background: rgba(255, 255, 255, 0.15); +} + +.btn-cyber { + background: linear-gradient(45deg, #00d2ff, #3a7bd5); + border: none; + border-radius: 12px; + color: #fff; + padding: 12px 30px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + transition: all 0.3s ease; +} + +.btn-cyber:hover { + box-shadow: 0 0 20px rgba(0, 210, 255, 0.4); + transform: scale(1.05); +} + +.headline-item { + padding: 12px; + border-bottom: 1px solid var(--glass-border); + font-size: 0.95rem; +} + +.headline-item:last-child { + border-bottom: none; +} + +.sentiment-badge { + padding: 4px 8px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 700; +} + +.badge-success { background: rgba(57, 255, 20, 0.2); color: var(--neon-green); border: 1px solid var(--neon-green); } +.badge-danger { background: rgba(255, 49, 49, 0.2); color: var(--vibrant-red); border: 1px solid var(--vibrant-red); } +.badge-secondary { background: rgba(136, 136, 136, 0.2); color: var(--text-muted); border: 1px solid var(--text-muted); } + +.hero-gradient { + background: radial-gradient(circle at top right, rgba(0, 210, 255, 0.1), transparent); + min-height: 100vh; +} \ No newline at end of file