Asish Karthikeya Gogineni commited on
Commit ·
3e30d53
0
Parent(s):
Deploy Sentinel AI from GitHub
Browse files- .gitattributes +24 -0
- .gitignore +18 -0
- .streamlit/config.toml +7 -0
- .streamlit/secrets.toml.example +7 -0
- Dockerfile +24 -0
- README.md +50 -0
- agents/data_analysis_agent.py +245 -0
- agents/orchestrator_v3.py +396 -0
- agents/tool_calling_agents.py +92 -0
- alerts.json +1102 -0
- alphavantage_mcp.py +213 -0
- app.py +407 -0
- app_command_center.py +318 -0
- create_dummy_db.py +36 -0
- debug_gemini.py +20 -0
- deploy_local.sh +45 -0
- deployment_guide.md +64 -0
- docker-compose.yml +78 -0
- linkedin_post.md +59 -0
- logo_helper.py +13 -0
- main.py +54 -0
- mcp_gateway.py +93 -0
- monitor.py +179 -0
- packages.txt +0 -0
- private_mcp.py +175 -0
- requirements.txt +15 -0
- start_all.sh +52 -0
- style.css +212 -0
- tavily_mcp.py +117 -0
- test_ollama.py +27 -0
- watchlist.json +1 -0
.gitattributes
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .gitattributes for Hugging Face Spaces
|
| 2 |
+
# This file tells git to use LFS for binary files
|
| 3 |
+
|
| 4 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
# Additional binary formats from HF defaults
|
| 10 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
venv/
|
| 6 |
+
.env
|
| 7 |
+
.DS_Store
|
| 8 |
+
|
| 9 |
+
# Logs
|
| 10 |
+
logs/
|
| 11 |
+
*.log
|
| 12 |
+
|
| 13 |
+
# Database
|
| 14 |
+
*.db
|
| 15 |
+
|
| 16 |
+
# IDE
|
| 17 |
+
.vscode/
|
| 18 |
+
.idea/
|
.streamlit/config.toml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[theme]
|
| 2 |
+
base="dark"
|
| 3 |
+
primaryColor="#3b82f6"
|
| 4 |
+
backgroundColor="#0b0e11"
|
| 5 |
+
secondaryBackgroundColor="#15191e"
|
| 6 |
+
textColor="#e2e8f0"
|
| 7 |
+
font="sans serif"
|
.streamlit/secrets.toml.example
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Streamlit secrets (TOML format)
|
| 2 |
+
# Add your API keys here
|
| 3 |
+
|
| 4 |
+
TAVILY_API_KEY = "your-tavily-api-key-here"
|
| 5 |
+
ALPHA_VANTAGE_API_KEY = "your-alpha-vantage-api-key-here"
|
| 6 |
+
GOOGLE_API_KEY = "your-google-api-key-here"
|
| 7 |
+
GROQ_API_KEY = "your-groq-api-key-here"
|
Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies (if any)
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
curl \
|
| 9 |
+
software-properties-common \
|
| 10 |
+
git \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Copy requirements first to leverage cache
|
| 14 |
+
COPY requirements.txt .
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# Copy the entire application
|
| 18 |
+
COPY . .
|
| 19 |
+
|
| 20 |
+
# Expose the port Hugging Face expects
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
|
| 23 |
+
# Run the orchestration script
|
| 24 |
+
CMD ["python", "main.py"]
|
README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Sentinel AI Financial Intelligence
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
sdk_version: 1.29.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Sentinel AI - Financial Intelligence Platform
|
| 14 |
+
|
| 15 |
+
Transform raw market data into actionable business insights with the power of AI. Analyze stocks, news, and portfolios automatically using intelligent agents.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- 🧠 **Intelligent Analysis**: AI automatically understands market structures and generates insights
|
| 20 |
+
- 📊 **Smart Visualizations**: Creates appropriate charts and graphs with interactive visualizations
|
| 21 |
+
- 🎯 **Actionable Recommendations**: Get specific, measurable recommendations based on data-driven insights
|
| 22 |
+
- 🚨 **Live Wire**: Real-time market alerts and trending information
|
| 23 |
+
|
| 24 |
+
## Technology Stack
|
| 25 |
+
|
| 26 |
+
- **Frontend**: Streamlit
|
| 27 |
+
- **AI/ML**: Google Gemini, LangGraph
|
| 28 |
+
- **Data Sources**: Alpha Vantage, Tavily Search
|
| 29 |
+
- **Architecture**: Multi-agent system with orchestrated workflows
|
| 30 |
+
|
| 31 |
+
## Configuration
|
| 32 |
+
|
| 33 |
+
Before running, you need to set up the following secrets in Hugging Face Spaces settings:
|
| 34 |
+
|
| 35 |
+
```toml
|
| 36 |
+
GOOGLE_API_KEY = "your-google-api-key"
|
| 37 |
+
ALPHA_VANTAGE_API_KEY = "your-alpha-vantage-key"
|
| 38 |
+
TAVILY_API_KEY = "your-tavily-api-key"
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## Local Development
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
pip install -r requirements.txt
|
| 45 |
+
streamlit run app.py
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## License
|
| 49 |
+
|
| 50 |
+
MIT License - See LICENSE file for details
|
agents/data_analysis_agent.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import plotly.express as px
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import re
|
| 7 |
+
from typing import TypedDict, Dict, Any, List
|
| 8 |
+
|
| 9 |
+
from langgraph.graph import StateGraph, END
|
| 10 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 11 |
+
|
| 12 |
+
logging.basicConfig(level=logging.INFO)
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
class AnalysisState(TypedDict):
|
| 16 |
+
dataframe: pd.DataFrame
|
| 17 |
+
dataset_info: Dict[str, Any]
|
| 18 |
+
insights: str
|
| 19 |
+
visualizations: List[Dict[str, Any]]
|
| 20 |
+
charts: List[Any]
|
| 21 |
+
|
| 22 |
+
class DataAnalysisAgent:
|
| 23 |
+
def __init__(self, llm: ChatGoogleGenerativeAI):
|
| 24 |
+
self.llm = llm
|
| 25 |
+
self.workflow = self._create_workflow()
|
| 26 |
+
|
| 27 |
+
def _create_workflow(self):
|
| 28 |
+
"""Creates the graph workflow for the data analysis sub-agent."""
|
| 29 |
+
workflow = StateGraph(AnalysisState)
|
| 30 |
+
workflow.add_node("data_profiler", self._profile_dataset)
|
| 31 |
+
# This new node will generate insights AND plan visualizations in one LLM call
|
| 32 |
+
workflow.add_node("insight_and_viz_planner", self._generate_insights_and_plan_visualizations)
|
| 33 |
+
workflow.add_node("chart_creator", self._create_charts)
|
| 34 |
+
|
| 35 |
+
workflow.add_edge("data_profiler", "insight_and_viz_planner")
|
| 36 |
+
workflow.add_edge("insight_and_viz_planner", "chart_creator")
|
| 37 |
+
workflow.add_edge("chart_creator", END)
|
| 38 |
+
|
| 39 |
+
workflow.set_entry_point("data_profiler")
|
| 40 |
+
return workflow.compile()
|
| 41 |
+
|
| 42 |
+
def _profile_dataset(self, state: AnalysisState):
|
| 43 |
+
"""Profiles the dataset to understand its structure for the LLM."""
|
| 44 |
+
logger.info("--- 📊 (Sub-Agent) Profiling Data ---")
|
| 45 |
+
df_for_profiling = state["dataframe"].copy().reset_index()
|
| 46 |
+
|
| 47 |
+
profile = {
|
| 48 |
+
"shape": df_for_profiling.shape,
|
| 49 |
+
"columns": list(df_for_profiling.columns),
|
| 50 |
+
"dtypes": {col: str(dtype) for col, dtype in df_for_profiling.dtypes.to_dict().items()},
|
| 51 |
+
"numeric_columns": df_for_profiling.select_dtypes(include=[np.number]).columns.tolist(),
|
| 52 |
+
"datetime_columns": df_for_profiling.select_dtypes(include=['datetime64']).columns.tolist()
|
| 53 |
+
}
|
| 54 |
+
logger.info(" Data profile created.")
|
| 55 |
+
return {"dataset_info": profile}
|
| 56 |
+
|
| 57 |
+
def _generate_insights_and_plan_visualizations(self, state: AnalysisState):
|
| 58 |
+
"""Generates key insights and plans visualizations in a single LLM call."""
|
| 59 |
+
logger.info("--- 🧠 (Sub-Agent) Generating Insights & Visualization Plan ---")
|
| 60 |
+
info = state["dataset_info"]
|
| 61 |
+
datetime_col = info.get("datetime_columns", [None])[0] or info.get("columns", ["index"])[0]
|
| 62 |
+
|
| 63 |
+
prompt = f"""
|
| 64 |
+
You are an expert financial data scientist. Based on the following data profile from a time-series stock dataset,
|
| 65 |
+
generate key insights and plan effective visualizations.
|
| 66 |
+
|
| 67 |
+
Data Profile: {json.dumps(info, indent=2)}
|
| 68 |
+
|
| 69 |
+
Instructions:
|
| 70 |
+
Your response MUST be ONLY a single valid JSON object. Do not include any other text or markdown.
|
| 71 |
+
The JSON object must have two keys: "insights" and "visualizations".
|
| 72 |
+
- "insights": A list of 3-5 concise, bullet-point style strings focusing on trends, correlations, and anomalies.
|
| 73 |
+
- "visualizations": A list of 3 JSON objects, each planning a chart.
|
| 74 |
+
- Plan a line chart for the 'close' price over time using the '{datetime_col}' column.
|
| 75 |
+
- Plan a histogram for the 'volume' column.
|
| 76 |
+
- Plan one other relevant chart (e.g., scatter plot, bar chart).
|
| 77 |
+
|
| 78 |
+
Example Response:
|
| 79 |
+
{{
|
| 80 |
+
"insights": [
|
| 81 |
+
"The closing price shows a significant upward trend over the period.",
|
| 82 |
+
"Trading volume spiked on dates corresponding to major news events.",
|
| 83 |
+
"There is a strong positive correlation between opening and closing prices."
|
| 84 |
+
],
|
| 85 |
+
"visualizations": [
|
| 86 |
+
{{"type": "line", "columns": ["{datetime_col}", "close"], "title": "Closing Price Over Time"}},
|
| 87 |
+
{{"type": "histogram", "columns": ["volume"], "title": "Trading Volume Distribution"}},
|
| 88 |
+
{{"type": "scatter", "columns": ["open", "close"], "title": "Opening vs. Closing Price"}}
|
| 89 |
+
]
|
| 90 |
+
}}
|
| 91 |
+
"""
|
| 92 |
+
response_str = self.llm.invoke(prompt).content
|
| 93 |
+
logger.info(f" LLM raw output for insights & viz plan:\n{response_str}")
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
json_match = re.search(r'\{.*\}', response_str, re.DOTALL)
|
| 97 |
+
if not json_match:
|
| 98 |
+
raise ValueError("No JSON object found in the LLM response.")
|
| 99 |
+
|
| 100 |
+
clean_json_str = json_match.group(0)
|
| 101 |
+
response_json = json.loads(clean_json_str)
|
| 102 |
+
|
| 103 |
+
insights_list = response_json.get("insights", [])
|
| 104 |
+
insights_str = "\n".join(f"* {insight}" for insight in insights_list)
|
| 105 |
+
viz_plan = response_json.get("visualizations", [])
|
| 106 |
+
|
| 107 |
+
logger.info(" Successfully parsed insights and viz plan.")
|
| 108 |
+
return {"insights": insights_str, "visualizations": viz_plan}
|
| 109 |
+
|
| 110 |
+
except (json.JSONDecodeError, ValueError) as e:
|
| 111 |
+
logger.error(f"Failed to parse insights and visualization plan from LLM. Error: {e}")
|
| 112 |
+
logger.info(" Using a default visualization plan as a fallback.")
|
| 113 |
+
default_plan = [
|
| 114 |
+
{"type": "line", "columns": [datetime_col, "close"], "title": "Closing Price Over Time (Default)"},
|
| 115 |
+
{"type": "histogram", "columns": ["volume"], "title": "Trading Volume (Default)"}
|
| 116 |
+
]
|
| 117 |
+
return {"insights": "Analysis generated, but detailed insights could not be parsed.", "visualizations": default_plan}
|
| 118 |
+
|
| 119 |
+
def _create_charts(self, state: AnalysisState):
|
| 120 |
+
"""Creates Plotly charts - HARDCODED for reliability."""
|
| 121 |
+
logger.info("--- 🎨 (Sub-Agent) Creating Charts ---")
|
| 122 |
+
|
| 123 |
+
# 1. Prepare DataFrame
|
| 124 |
+
df = state["dataframe"].copy()
|
| 125 |
+
if df.index.name in ['timestamp', 'date', 'datetime', 'index']:
|
| 126 |
+
df = df.reset_index()
|
| 127 |
+
|
| 128 |
+
# Normalize column names to lowercase
|
| 129 |
+
df.columns = [str(c).lower() for c in df.columns]
|
| 130 |
+
|
| 131 |
+
charts = []
|
| 132 |
+
|
| 133 |
+
# Find X-axis column (timestamp)
|
| 134 |
+
x_col = None
|
| 135 |
+
for candidate in ['timestamp', 'date', 'datetime', 'index']:
|
| 136 |
+
if candidate in df.columns:
|
| 137 |
+
x_col = candidate
|
| 138 |
+
break
|
| 139 |
+
|
| 140 |
+
if not x_col:
|
| 141 |
+
logger.warning(" No timestamp column found. Skipping charts.")
|
| 142 |
+
return {"charts": []}
|
| 143 |
+
|
| 144 |
+
# --- CHART 1: Price History (Line) ---
|
| 145 |
+
if 'close' in df.columns:
|
| 146 |
+
try:
|
| 147 |
+
logger.info(f" Generating Price Chart (x={x_col}, y=close)")
|
| 148 |
+
fig = px.line(df, x=x_col, y='close',
|
| 149 |
+
title="📈 Price History",
|
| 150 |
+
template="plotly_dark",
|
| 151 |
+
labels={'close': 'Price ($)', x_col: 'Time'})
|
| 152 |
+
fig.update_traces(line_color='#00ff41')
|
| 153 |
+
charts.append(fig)
|
| 154 |
+
except Exception as e:
|
| 155 |
+
logger.error(f" Failed to generate price chart: {e}")
|
| 156 |
+
|
| 157 |
+
# --- CHART 2: Volume (Bar) ---
|
| 158 |
+
if 'volume' in df.columns:
|
| 159 |
+
try:
|
| 160 |
+
logger.info(f" Generating Volume Chart (x={x_col}, y=volume)")
|
| 161 |
+
fig = px.bar(df, x=x_col, y='volume',
|
| 162 |
+
title="📊 Trading Volume",
|
| 163 |
+
template="plotly_dark",
|
| 164 |
+
labels={'volume': 'Volume', x_col: 'Time'})
|
| 165 |
+
fig.update_traces(marker_color='#ff6b35')
|
| 166 |
+
charts.append(fig)
|
| 167 |
+
except Exception as e:
|
| 168 |
+
logger.error(f" Failed to generate volume chart: {e}")
|
| 169 |
+
|
| 170 |
+
# --- CHART 3: Price vs Volume (Scatter) ---
|
| 171 |
+
if 'close' in df.columns and 'volume' in df.columns:
|
| 172 |
+
try:
|
| 173 |
+
logger.info(" Generating Price vs Volume Scatter Plot")
|
| 174 |
+
fig = px.scatter(df, x='volume', y='close',
|
| 175 |
+
title="🔍 Price vs Volume Correlation",
|
| 176 |
+
template="plotly_dark",
|
| 177 |
+
labels={'volume': 'Trading Volume', 'close': 'Price ($)'},
|
| 178 |
+
trendline="ols", # Add regression line
|
| 179 |
+
opacity=0.6)
|
| 180 |
+
fig.update_traces(marker=dict(size=8, color='#4ecdc4'))
|
| 181 |
+
charts.append(fig)
|
| 182 |
+
except Exception as e:
|
| 183 |
+
logger.error(f" Failed to generate scatter plot: {e}")
|
| 184 |
+
|
| 185 |
+
# --- CHART 4: Daily Returns Histogram ---
|
| 186 |
+
if 'close' in df.columns and len(df) > 1:
|
| 187 |
+
try:
|
| 188 |
+
logger.info(" Generating Daily Returns Histogram")
|
| 189 |
+
# Calculate returns
|
| 190 |
+
df['returns'] = df['close'].pct_change() * 100
|
| 191 |
+
df_returns = df.dropna(subset=['returns'])
|
| 192 |
+
|
| 193 |
+
if not df_returns.empty:
|
| 194 |
+
fig = px.histogram(df_returns, x='returns',
|
| 195 |
+
nbins=30,
|
| 196 |
+
title="📊 Daily Returns Distribution",
|
| 197 |
+
template="plotly_dark",
|
| 198 |
+
labels={'returns': 'Daily Return (%)'},
|
| 199 |
+
color_discrete_sequence=['#9b59b6'])
|
| 200 |
+
fig.add_vline(x=0, line_dash="dash", line_color="white",
|
| 201 |
+
annotation_text="Zero Return", annotation_position="top")
|
| 202 |
+
charts.append(fig)
|
| 203 |
+
except Exception as e:
|
| 204 |
+
logger.error(f" Failed to generate histogram: {e}")
|
| 205 |
+
|
| 206 |
+
# --- CHART 5: Box Plot (Price Distribution) ---
|
| 207 |
+
if 'close' in df.columns:
|
| 208 |
+
try:
|
| 209 |
+
logger.info(" Generating Box Plot")
|
| 210 |
+
fig = px.box(df, y='close',
|
| 211 |
+
title="📦 Price Distribution (Box Plot)",
|
| 212 |
+
template="plotly_dark",
|
| 213 |
+
labels={'close': 'Price ($)'},
|
| 214 |
+
color_discrete_sequence=['#a29bfe'])
|
| 215 |
+
charts.append(fig)
|
| 216 |
+
except Exception as e:
|
| 217 |
+
logger.error(f" Failed to generate box plot: {e}")
|
| 218 |
+
|
| 219 |
+
# --- CHART 6: Violin Plot (Volume Distribution) ---
|
| 220 |
+
if 'volume' in df.columns:
|
| 221 |
+
try:
|
| 222 |
+
logger.info(" Generating Violin Plot")
|
| 223 |
+
fig = px.violin(df, y='volume',
|
| 224 |
+
title="🎻 Volume Distribution (Violin Plot)",
|
| 225 |
+
template="plotly_dark",
|
| 226 |
+
labels={'volume': 'Trading Volume'},
|
| 227 |
+
color_discrete_sequence=['#74b9ff'],
|
| 228 |
+
box=True, # Show box plot inside violin
|
| 229 |
+
points='all') # Show all data points
|
| 230 |
+
charts.append(fig)
|
| 231 |
+
except Exception as e:
|
| 232 |
+
logger.error(f" Failed to generate violin plot: {e}")
|
| 233 |
+
|
| 234 |
+
logger.info(f" Successfully created {len(charts)} charts.")
|
| 235 |
+
return {"charts": charts}
|
| 236 |
+
|
| 237 |
+
def run_analysis(self, dataframe: pd.DataFrame):
|
| 238 |
+
"""Runs the full analysis workflow on the given DataFrame."""
|
| 239 |
+
if dataframe.empty:
|
| 240 |
+
logger.warning("Input DataFrame is empty. Skipping analysis.")
|
| 241 |
+
return {"insights": "No data available for analysis.", "charts": []}
|
| 242 |
+
initial_state = {"dataframe": dataframe}
|
| 243 |
+
# The final state will now contain insights and charts after the workflow runs
|
| 244 |
+
final_state = self.workflow.invoke(initial_state)
|
| 245 |
+
return final_state
|
agents/orchestrator_v3.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import ast
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from typing import TypedDict, List, Dict, Any
|
| 7 |
+
|
| 8 |
+
from langgraph.graph import StateGraph, END
|
| 9 |
+
|
| 10 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 11 |
+
|
| 12 |
+
from agents.tool_calling_agents import WebResearchAgent, MarketDataAgent, InternalPortfolioAgent
|
| 13 |
+
from agents.data_analysis_agent import DataAnalysisAgent
|
| 14 |
+
|
| 15 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 16 |
+
|
| 17 |
+
# --- Configuration ---
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
# --- Initialize workers (Stateless) ---
|
| 21 |
+
web_agent = WebResearchAgent()
|
| 22 |
+
market_agent = MarketDataAgent()
|
| 23 |
+
portfolio_agent = InternalPortfolioAgent()
|
| 24 |
+
|
| 25 |
+
# --- Define the Enhanced State ---
|
| 26 |
+
class AgentState(TypedDict):
|
| 27 |
+
task: str
|
| 28 |
+
symbol: str
|
| 29 |
+
web_research_results: str
|
| 30 |
+
market_data_results: str
|
| 31 |
+
portfolio_data_results: str
|
| 32 |
+
scan_intent: str # "DOWNWARD", "UPWARD", "ALL", or None
|
| 33 |
+
# --- NEW FIELDS FOR ANALYSIS ---
|
| 34 |
+
analysis_dataframe: pd.DataFrame
|
| 35 |
+
analysis_results: Dict[str, Any]
|
| 36 |
+
final_report: str
|
| 37 |
+
# Debug fields
|
| 38 |
+
debug_market_data_raw: Any
|
| 39 |
+
debug_dataframe_head: Any
|
| 40 |
+
debug_analysis_results_full: Any
|
| 41 |
+
|
| 42 |
+
def get_orchestrator(llm_provider="gemini", api_key=None):
|
| 43 |
+
"""
|
| 44 |
+
Factory function to create the orchestrator graph with a specific LLM.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
# 1. Initialize LLM (Gemini Only)
|
| 48 |
+
if not api_key:
|
| 49 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 50 |
+
if not api_key:
|
| 51 |
+
raise ValueError("Google Gemini API Key is missing.")
|
| 52 |
+
llm = ChatGoogleGenerativeAI(model="gemini-flash-lite-latest", google_api_key=api_key, temperature=0, max_retries=5)
|
| 53 |
+
|
| 54 |
+
# 2. Initialize Data Analyzer with the chosen LLM
|
| 55 |
+
data_analyzer = DataAnalysisAgent(llm=llm)
|
| 56 |
+
|
| 57 |
+
# 3. Define Nodes (Closure captures 'llm' and 'data_analyzer')
|
| 58 |
+
|
| 59 |
+
# 3. Define Nodes (Closure captures 'llm' and 'data_analyzer')
|
| 60 |
+
|
| 61 |
+
def extract_symbol_step(state: AgentState):
|
| 62 |
+
print("--- 🔬 Symbol & Time Range Extraction ---")
|
| 63 |
+
prompt = f"""
|
| 64 |
+
Analyze the user's request: "{state['task']}"
|
| 65 |
+
|
| 66 |
+
Extract TWO things:
|
| 67 |
+
1. Stock symbol or scan intent
|
| 68 |
+
2. Time range (if mentioned)
|
| 69 |
+
|
| 70 |
+
RULES:
|
| 71 |
+
- If request mentions a SPECIFIC company → Extract symbol
|
| 72 |
+
- If request mentions time period → Extract time range
|
| 73 |
+
- ONLY set scan_intent for "top gainers", "losers", "scan market"
|
| 74 |
+
|
| 75 |
+
Response Format: JSON ONLY.
|
| 76 |
+
{{
|
| 77 |
+
"symbol": "TICKER" or null,
|
| 78 |
+
"scan_intent": "DOWNWARD" | "UPWARD" | "ALL" or null,
|
| 79 |
+
"time_range": "INTRADAY" | "1D" | "3D" | "1W" | "1M" | "3M" | "1Y" or null
|
| 80 |
+
}}
|
| 81 |
+
|
| 82 |
+
Time Range Examples:
|
| 83 |
+
- "today", "now", "current", "recent" → "INTRADAY"
|
| 84 |
+
- "yesterday", "1 day back" → "1D"
|
| 85 |
+
- "3 days back", "last 3 days" → "3D"
|
| 86 |
+
- "last week", "1 week", "7 days" → "1W"
|
| 87 |
+
- "last month", "1 month", "30 days" → "1M"
|
| 88 |
+
- "3 months", "quarter" → "3M"
|
| 89 |
+
- "1 year", "12 months" → "1Y"
|
| 90 |
+
|
| 91 |
+
Full Examples:
|
| 92 |
+
- "Analyze Tesla" → {{"symbol": "TSLA", "scan_intent": null, "time_range": null}}
|
| 93 |
+
- "3 days back stocks of Tesla" → {{"symbol": "TSLA", "scan_intent": null, "time_range": "3D"}}
|
| 94 |
+
- "Last week AAPL performance" → {{"symbol": "AAPL", "scan_intent": null, "time_range": "1W"}}
|
| 95 |
+
- "1 month trend for NVDA" → {{"symbol": "NVDA", "scan_intent": null, "time_range": "1M"}}
|
| 96 |
+
- "Recent analysis of Tesla" → {{"symbol": "TSLA", "scan_intent": null, "time_range": "INTRADAY"}}
|
| 97 |
+
- "Show me top gainers" → {{"symbol": null, "scan_intent": "UPWARD", "time_range": null}}
|
| 98 |
+
|
| 99 |
+
CRITICAL: Default to null for time_range if not explicitly mentioned!
|
| 100 |
+
"""
|
| 101 |
+
raw_response = llm.invoke(prompt).content.strip()
|
| 102 |
+
|
| 103 |
+
symbol = None
|
| 104 |
+
scan_intent = None
|
| 105 |
+
time_range = None
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
import json
|
| 109 |
+
import re
|
| 110 |
+
# Find JSON in response
|
| 111 |
+
json_match = re.search(r'\{.*\}', raw_response, re.DOTALL)
|
| 112 |
+
if json_match:
|
| 113 |
+
data = json.loads(json_match.group(0))
|
| 114 |
+
symbol = data.get("symbol")
|
| 115 |
+
scan_intent = data.get("scan_intent")
|
| 116 |
+
time_range = data.get("time_range")
|
| 117 |
+
else:
|
| 118 |
+
print(f" WARNING: No JSON found in extraction response: {raw_response}")
|
| 119 |
+
# Fallback to simple cleaning
|
| 120 |
+
clean_resp = raw_response.strip().upper()
|
| 121 |
+
if "SCAN" in clean_resp or "GAINERS" in clean_resp or "LOSERS" in clean_resp:
|
| 122 |
+
scan_intent = "ALL"
|
| 123 |
+
elif len(clean_resp) <= 5 and clean_resp.isalpha():
|
| 124 |
+
symbol = clean_resp
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f" Error parsing symbol extraction: {e}")
|
| 127 |
+
|
| 128 |
+
if symbol: symbol = symbol.upper().replace("$", "")
|
| 129 |
+
|
| 130 |
+
# Default time_range to INTRADAY if null (for backward compatibility)
|
| 131 |
+
if time_range is None:
|
| 132 |
+
time_range = "INTRADAY"
|
| 133 |
+
|
| 134 |
+
print(f" Raw LLM Response: {raw_response}")
|
| 135 |
+
print(f" Extracted Symbol: {symbol}")
|
| 136 |
+
print(f" Scan Intent: {scan_intent}")
|
| 137 |
+
print(f" Time Range: {time_range}")
|
| 138 |
+
|
| 139 |
+
return {"symbol": symbol, "scan_intent": scan_intent, "time_range": time_range}
|
| 140 |
+
|
| 141 |
+
def web_research_step(state: AgentState):
|
| 142 |
+
print("--- 🔎 Web Research ---")
|
| 143 |
+
if state.get("scan_intent"):
|
| 144 |
+
return {"web_research_results": "Market Scan initiated. Web research skipped for individual stock."}
|
| 145 |
+
results = web_agent.research(queries=[state['task']])
|
| 146 |
+
return {"web_research_results": results}
|
| 147 |
+
|
| 148 |
+
def market_data_step(state: AgentState):
|
| 149 |
+
print("--- 📊 Market Data Retrieval ---")
|
| 150 |
+
|
| 151 |
+
# Handle scan intent
|
| 152 |
+
if state.get("scan_intent"):
|
| 153 |
+
print(f" Scan Intent Detected: {state['scan_intent']}")
|
| 154 |
+
|
| 155 |
+
# Load watchlist
|
| 156 |
+
import json
|
| 157 |
+
watchlist_path = "watchlist.json"
|
| 158 |
+
if not os.path.exists(watchlist_path):
|
| 159 |
+
return {"market_data_results": {"error": "Watchlist not found. Please add symbols to your watchlist."}}
|
| 160 |
+
|
| 161 |
+
with open(watchlist_path, 'r') as f:
|
| 162 |
+
watchlist = json.load(f)
|
| 163 |
+
|
| 164 |
+
scan_results = []
|
| 165 |
+
scan_intent = state['scan_intent']
|
| 166 |
+
|
| 167 |
+
for sym in watchlist:
|
| 168 |
+
# Get compact data for speed (always use INTRADAY for scans)
|
| 169 |
+
data = market_agent.get_market_data(symbol=sym, time_range="INTRADAY")
|
| 170 |
+
if isinstance(data, dict) and 'data' in data:
|
| 171 |
+
ts = data['data']
|
| 172 |
+
sorted_times = sorted(ts.keys())
|
| 173 |
+
if len(sorted_times) > 0:
|
| 174 |
+
latest_time = sorted_times[-1]
|
| 175 |
+
earliest_time = sorted_times[0]
|
| 176 |
+
latest_close = float(ts[latest_time]['4. close'])
|
| 177 |
+
earliest_open = float(ts[earliest_time]['1. open'])
|
| 178 |
+
pct_change = ((latest_close - earliest_open) / earliest_open) * 100
|
| 179 |
+
|
| 180 |
+
# Filter based on scan intent
|
| 181 |
+
if scan_intent == "UPWARD" and pct_change > 0:
|
| 182 |
+
scan_results.append({"symbol": sym, "price": latest_close, "change": pct_change})
|
| 183 |
+
elif scan_intent == "DOWNWARD" and pct_change < 0:
|
| 184 |
+
scan_results.append({"symbol": sym, "price": latest_close, "change": pct_change})
|
| 185 |
+
elif scan_intent == "ALL":
|
| 186 |
+
scan_results.append({"symbol": sym, "price": latest_close, "change": pct_change})
|
| 187 |
+
|
| 188 |
+
# Sort by change
|
| 189 |
+
scan_results.sort(key=lambda x: x['change'], reverse=True)
|
| 190 |
+
return {"market_data_results": {"scan_results": scan_results}}
|
| 191 |
+
|
| 192 |
+
# Single symbol analysis
|
| 193 |
+
if not state.get("symbol"):
|
| 194 |
+
return {"market_data_results": "Skipped."}
|
| 195 |
+
|
| 196 |
+
time_range = state.get("time_range", "INTRADAY")
|
| 197 |
+
print(f" Fetching market data for {state['symbol']} (time_range={time_range})")
|
| 198 |
+
results = market_agent.get_market_data(symbol=state["symbol"], time_range=time_range)
|
| 199 |
+
return {"market_data_results": results, "debug_market_data_raw": results}
|
| 200 |
+
|
| 201 |
+
def portfolio_data_step(state: AgentState):
|
| 202 |
+
print("--- 💼 Internal Portfolio Data ---")
|
| 203 |
+
if state.get("scan_intent"):
|
| 204 |
+
return {"portfolio_data_results": "Market Scan initiated. Portfolio context skipped."}
|
| 205 |
+
|
| 206 |
+
if not state.get("symbol"):
|
| 207 |
+
return {"portfolio_data_results": "Skipped: No symbol provided."}
|
| 208 |
+
|
| 209 |
+
results = portfolio_agent.query_portfolio(question=f"What is the current exposure to {state['symbol']}?")
|
| 210 |
+
return {"portfolio_data_results": results}
|
| 211 |
+
|
| 212 |
+
def transform_data_step(state: AgentState):
|
| 213 |
+
print("--- 🔀 Transforming Data for Analysis ---")
|
| 214 |
+
if state.get("scan_intent"):
|
| 215 |
+
return {"analysis_dataframe": pd.DataFrame()} # Skip transformation for scan
|
| 216 |
+
|
| 217 |
+
market_data = state.get("market_data_results")
|
| 218 |
+
|
| 219 |
+
if not isinstance(market_data, dict) or not market_data.get('data'):
|
| 220 |
+
print(" Skipping transformation: No valid market data received.")
|
| 221 |
+
return {"analysis_dataframe": pd.DataFrame()}
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
time_series_data = market_data.get('data')
|
| 225 |
+
if not time_series_data:
|
| 226 |
+
raise ValueError("The 'data' key is empty.")
|
| 227 |
+
|
| 228 |
+
df = pd.DataFrame.from_dict(time_series_data, orient='index')
|
| 229 |
+
df.index = pd.to_datetime(df.index)
|
| 230 |
+
df.index.name = "timestamp"
|
| 231 |
+
df.rename(columns={
|
| 232 |
+
'1. open': 'open', '2. high': 'high', '3. low': 'low',
|
| 233 |
+
'4. close': 'close', '5. volume': 'volume'
|
| 234 |
+
}, inplace=True)
|
| 235 |
+
df = df.apply(pd.to_numeric).sort_index()
|
| 236 |
+
|
| 237 |
+
print(f" Successfully created DataFrame with shape {df.shape}")
|
| 238 |
+
return {"analysis_dataframe": df, "debug_dataframe_head": df.head().to_dict()}
|
| 239 |
+
except Exception as e:
|
| 240 |
+
print(f" CRITICAL ERROR during data transformation: {e}")
|
| 241 |
+
return {"analysis_dataframe": pd.DataFrame()}
|
| 242 |
+
|
| 243 |
+
def run_data_analysis_step(state: AgentState):
|
| 244 |
+
print("--- 🔬 Running Deep-Dive Data Analysis ---")
|
| 245 |
+
if state.get("scan_intent"):
|
| 246 |
+
return {"analysis_results": {}} # Skip analysis for scan
|
| 247 |
+
|
| 248 |
+
df = state.get("analysis_dataframe")
|
| 249 |
+
if df is not None and not df.empty:
|
| 250 |
+
analysis_results = data_analyzer.run_analysis(df)
|
| 251 |
+
return {"analysis_results": analysis_results, "debug_analysis_results_full": analysis_results}
|
| 252 |
+
else:
|
| 253 |
+
print(" Skipping analysis: No data to analyze.")
|
| 254 |
+
return {"analysis_results": {}}
|
| 255 |
+
|
| 256 |
+
def synthesize_report_step(state: AgentState):
|
| 257 |
+
print("--- 📝 Synthesizing Final Report ---")
|
| 258 |
+
|
| 259 |
+
# Helper to truncate text to avoid Rate Limits
|
| 260 |
+
def truncate(text, max_chars=3000):
|
| 261 |
+
s = str(text)
|
| 262 |
+
if len(s) > max_chars:
|
| 263 |
+
return s[:max_chars] + "... (truncated)"
|
| 264 |
+
return s
|
| 265 |
+
|
| 266 |
+
# Check for Scan Results
|
| 267 |
+
market_data_res = state.get("market_data_results", {})
|
| 268 |
+
if isinstance(market_data_res, dict) and "scan_results" in market_data_res:
|
| 269 |
+
scan_results = market_data_res["scan_results"]
|
| 270 |
+
# Truncate scan results if necessary (though usually small)
|
| 271 |
+
scan_results_str = truncate(scan_results, 4000)
|
| 272 |
+
|
| 273 |
+
report_prompt = f"""
|
| 274 |
+
You are a senior financial analyst. The user requested a market scan: "{state['task']}".
|
| 275 |
+
|
| 276 |
+
Scan Results (from Watchlist):
|
| 277 |
+
{scan_results_str}
|
| 278 |
+
|
| 279 |
+
Generate a "Market Scan Report".
|
| 280 |
+
1. Summary: Briefly explain the criteria and the overall market status based on these results.
|
| 281 |
+
2. Results Table: Create a markdown table with columns: Symbol | Price | % Change.
|
| 282 |
+
3. Conclusion: Highlight the most significant movers.
|
| 283 |
+
"""
|
| 284 |
+
final_report = llm.invoke(report_prompt).content
|
| 285 |
+
return {"final_report": final_report}
|
| 286 |
+
|
| 287 |
+
analysis_insights = state.get("analysis_results", {}).get("insights", "Not available.")
|
| 288 |
+
|
| 289 |
+
# Truncate inputs for the main report
|
| 290 |
+
web_data = truncate(state.get('web_research_results', 'Not available.'), 3000)
|
| 291 |
+
market_summary = truncate(state.get('market_data_results', 'Not available'), 2000)
|
| 292 |
+
portfolio_data = truncate(state.get('portfolio_data_results', 'Not available.'), 2000)
|
| 293 |
+
|
| 294 |
+
# Extract Data Source
|
| 295 |
+
market_data_raw = state.get("market_data_results", {})
|
| 296 |
+
data_source = "Unknown"
|
| 297 |
+
if isinstance(market_data_raw, dict):
|
| 298 |
+
meta = market_data_raw.get("meta_data", {})
|
| 299 |
+
if isinstance(meta, dict):
|
| 300 |
+
data_source = meta.get("Source", "Real API (Alpha Vantage)")
|
| 301 |
+
|
| 302 |
+
report_prompt = f"""
|
| 303 |
+
You are a senior financial analyst writing a comprehensive "Alpha Report".
|
| 304 |
+
Your task is to synthesize all available information into a structured, cited report.
|
| 305 |
+
|
| 306 |
+
Original User Task: {state['task']}
|
| 307 |
+
Target Symbol: {state.get('symbol', 'Unknown')}
|
| 308 |
+
Data Source: {data_source}
|
| 309 |
+
---
|
| 310 |
+
Available Information:
|
| 311 |
+
- Web Intelligence: {web_data}
|
| 312 |
+
- Market Data Summary: {market_summary}
|
| 313 |
+
- Deep-Dive Data Analysis Insights: {analysis_insights}
|
| 314 |
+
- Internal Portfolio Context: {portfolio_data}
|
| 315 |
+
---
|
| 316 |
+
|
| 317 |
+
CRITICAL INSTRUCTIONS:
|
| 318 |
+
1. First, evaluate the "Available Information".
|
| 319 |
+
- If the Target Symbol is 'Unknown' OR if the Web Intelligence and Market Data contain no meaningful information:
|
| 320 |
+
You MUST respond with: "I am not sure about this company as I could not find sufficient data."
|
| 321 |
+
Do NOT generate the rest of the report.
|
| 322 |
+
|
| 323 |
+
2. Otherwise, generate the "Alpha Report" with the following sections:
|
| 324 |
+
|
| 325 |
+
> [!NOTE]
|
| 326 |
+
> **Data Source**: {data_source}
|
| 327 |
+
|
| 328 |
+
## 1. Executive Summary
|
| 329 |
+
A 2-3 sentence overview of the key findings and current situation.
|
| 330 |
+
|
| 331 |
+
## 2. Internal Context
|
| 332 |
+
Detail the firm's current exposure:
|
| 333 |
+
- IF the firm has shares > 0: Present as a markdown table:
|
| 334 |
+
| Symbol | Shares | Avg Cost | Current Value |
|
| 335 |
+
|--------|--------|----------|---------------|
|
| 336 |
+
- IF the firm has 0 shares: State: "The firm has no current exposure to {state.get('symbol')}."
|
| 337 |
+
|
| 338 |
+
## 3. Market Data
|
| 339 |
+
ALWAYS present as a markdown table:
|
| 340 |
+
| Metric | Value | Implication |
|
| 341 |
+
|--------|-------|-------------|
|
| 342 |
+
| Current Price | $XXX.XX | +/-X.X% vs. open |
|
| 343 |
+
| 5-Day Trend | Upward/Downward/Flat | Brief note |
|
| 344 |
+
| Volume | X.XXM | Above/Below average |
|
| 345 |
+
|
| 346 |
+
## 4. Real-Time Intelligence
|
| 347 |
+
### News
|
| 348 |
+
- **[Headline]** - [Brief summary] `[Source: URL]`
|
| 349 |
+
- **[Headline]** - [Brief summary] `[Source: URL]`
|
| 350 |
+
|
| 351 |
+
### Filings (if any)
|
| 352 |
+
- **[Filing Type]** - [Brief description] `[Source: URL]`
|
| 353 |
+
|
| 354 |
+
## 5. Sentiment Analysis
|
| 355 |
+
**Overall Sentiment:** Bullish / Bearish / Neutral
|
| 356 |
+
|
| 357 |
+
**Evidence:**
|
| 358 |
+
- [Specific fact from news/data supporting this sentiment]
|
| 359 |
+
- [Another supporting fact]
|
| 360 |
+
|
| 361 |
+
## 6. Synthesis & Recommendations
|
| 362 |
+
Combine all information to provide actionable insights. Focus on:
|
| 363 |
+
- Key risks and opportunities
|
| 364 |
+
- Recommended actions (if any)
|
| 365 |
+
- Items to monitor
|
| 366 |
+
|
| 367 |
+
FORMATTING RULES:
|
| 368 |
+
- Use markdown headers (##, ###)
|
| 369 |
+
- Include URLs in backticks: `[Source: example.com]`
|
| 370 |
+
- Use tables for structured data
|
| 371 |
+
- Be concise but comprehensive
|
| 372 |
+
"""
|
| 373 |
+
final_report = llm.invoke(report_prompt).content
|
| 374 |
+
return {"final_report": final_report}
|
| 375 |
+
|
| 376 |
+
# 4. Build the Graph
|
| 377 |
+
workflow = StateGraph(AgentState)
|
| 378 |
+
|
| 379 |
+
workflow.add_node("extract_symbol", extract_symbol_step)
|
| 380 |
+
workflow.add_node("web_researcher", web_research_step)
|
| 381 |
+
workflow.add_node("market_data_analyst", market_data_step)
|
| 382 |
+
workflow.add_node("portfolio_data_fetcher", portfolio_data_step)
|
| 383 |
+
workflow.add_node("transform_data", transform_data_step)
|
| 384 |
+
workflow.add_node("data_analyzer", run_data_analysis_step)
|
| 385 |
+
workflow.add_node("report_synthesizer", synthesize_report_step)
|
| 386 |
+
|
| 387 |
+
workflow.set_entry_point("extract_symbol")
|
| 388 |
+
workflow.add_edge("extract_symbol", "web_researcher")
|
| 389 |
+
workflow.add_edge("web_researcher", "market_data_analyst")
|
| 390 |
+
workflow.add_edge("market_data_analyst", "portfolio_data_fetcher")
|
| 391 |
+
workflow.add_edge("portfolio_data_fetcher", "transform_data")
|
| 392 |
+
workflow.add_edge("transform_data", "data_analyzer")
|
| 393 |
+
workflow.add_edge("data_analyzer", "report_synthesizer")
|
| 394 |
+
workflow.add_edge("report_synthesizer", END)
|
| 395 |
+
|
| 396 |
+
return workflow.compile()
|
agents/tool_calling_agents.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# agents/tool_calling_agents.py (Corrected with longer timeout)
|
| 2 |
+
import httpx
|
| 3 |
+
import logging
|
| 4 |
+
|
| 5 |
+
# --- Configuration ---
|
| 6 |
+
import os
|
| 7 |
+
MCP_GATEWAY_URL = os.getenv("MCP_GATEWAY_URL", "http://127.0.0.1:8000/route_agent_request")
|
| 8 |
+
|
| 9 |
+
# --- Logging Setup ---
|
| 10 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 11 |
+
logger = logging.getLogger("ToolCallingAgents")
|
| 12 |
+
|
| 13 |
+
class BaseAgent:
|
| 14 |
+
"""A base class for agents that call tools via the MCP Gateway."""
|
| 15 |
+
def __init__(self):
|
| 16 |
+
# A reasonable default timeout for fast, external APIs
|
| 17 |
+
self.client = httpx.Client(timeout=30.0)
|
| 18 |
+
|
| 19 |
+
def call_mcp_gateway(self, target_service: str, payload: dict) -> dict:
|
| 20 |
+
"""A standardized method to make a request to the MCP Gateway."""
|
| 21 |
+
request_body = { "target_service": target_service, "payload": payload }
|
| 22 |
+
try:
|
| 23 |
+
logger.info(f"Agent calling MCP Gateway for service '{target_service}' with payload: {payload}")
|
| 24 |
+
response = self.client.post(MCP_GATEWAY_URL, json=request_body)
|
| 25 |
+
response.raise_for_status()
|
| 26 |
+
logger.info(f"Received successful response from MCP Gateway for '{target_service}'.")
|
| 27 |
+
return response.json()
|
| 28 |
+
except httpx.HTTPStatusError as e:
|
| 29 |
+
logger.error(f"Error response {e.response.status_code} from MCP Gateway: {e.response.text}")
|
| 30 |
+
raise
|
| 31 |
+
except httpx.RequestError as e:
|
| 32 |
+
logger.error(f"Failed to connect to MCP Gateway at {MCP_GATEWAY_URL}: {e}")
|
| 33 |
+
raise
|
| 34 |
+
|
| 35 |
+
class WebResearchAgent(BaseAgent):
|
| 36 |
+
"""An agent specialized in performing web research using Tavily."""
|
| 37 |
+
def research(self, queries: list[str], search_depth: str = "basic") -> dict:
|
| 38 |
+
payload = { "queries": queries, "search_depth": search_depth }
|
| 39 |
+
return self.call_mcp_gateway("tavily_research", payload)
|
| 40 |
+
|
| 41 |
+
class MarketDataAgent(BaseAgent):
|
| 42 |
+
"""An agent specialized in fetching financial market data."""
|
| 43 |
+
def get_market_data(self, symbol: str, time_range: str = "INTRADAY") -> dict:
|
| 44 |
+
payload = { "symbol": symbol, "time_range": time_range }
|
| 45 |
+
return self.call_mcp_gateway("alpha_vantage_market_data", payload)
|
| 46 |
+
|
| 47 |
+
class InternalPortfolioAgent(BaseAgent):
|
| 48 |
+
"""An agent specialized in securely querying the internal portfolio database."""
|
| 49 |
+
|
| 50 |
+
# --- THIS IS THE FIX ---
|
| 51 |
+
def __init__(self):
|
| 52 |
+
# Override the default client with one that has a longer timeout
|
| 53 |
+
# because local LLM calls can be slow.
|
| 54 |
+
super().__init__()
|
| 55 |
+
self.client = httpx.Client(timeout=180.0) # Give it 180 seconds
|
| 56 |
+
|
| 57 |
+
def query_portfolio(self, question: str) -> dict:
|
| 58 |
+
payload = { "question": question }
|
| 59 |
+
return self.call_mcp_gateway("internal_portfolio_data", payload)
|
| 60 |
+
|
| 61 |
+
# --- Example Usage (for testing this file directly) ---
|
| 62 |
+
if __name__ == '__main__':
|
| 63 |
+
print("--- Testing Agents ---")
|
| 64 |
+
|
| 65 |
+
# Make sure all your MCP servers and the gateway are running.
|
| 66 |
+
|
| 67 |
+
# 1. Test the Web Research Agent
|
| 68 |
+
print("\n[1] Testing Web Research Agent...")
|
| 69 |
+
try:
|
| 70 |
+
web_agent = WebResearchAgent()
|
| 71 |
+
research_results = web_agent.research(queries=["What is the current market sentiment on NVIDIA?"])
|
| 72 |
+
print("Web Research Result:", research_results['status'])
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print("Web Research Agent failed:", e)
|
| 75 |
+
|
| 76 |
+
# 2. Test the Market Data Agent
|
| 77 |
+
print("\n[2] Testing Market Data Agent...")
|
| 78 |
+
try:
|
| 79 |
+
market_agent = MarketDataAgent()
|
| 80 |
+
market_results = market_agent.get_intraday_data(symbol="TSLA", interval="15min")
|
| 81 |
+
print("Market Data Result:", market_results['status'])
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print("Market Data Agent failed:", e)
|
| 84 |
+
|
| 85 |
+
# 3. Test the Internal Portfolio Agent
|
| 86 |
+
print("\n[3] Testing Internal Portfolio Agent...")
|
| 87 |
+
try:
|
| 88 |
+
portfolio_agent = InternalPortfolioAgent()
|
| 89 |
+
portfolio_results = portfolio_agent.query_portfolio(question="How many shares of AAPL do we own?")
|
| 90 |
+
print("Portfolio Query Result:", portfolio_results['status'])
|
| 91 |
+
except Exception as e:
|
| 92 |
+
print("Internal Portfolio Agent failed:", e)
|
alerts.json
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"timestamp": "2025-12-07T00:21:28.690106",
|
| 4 |
+
"type": "MARKET",
|
| 5 |
+
"symbol": "AMZN",
|
| 6 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.06% to $162.71",
|
| 7 |
+
"details": {
|
| 8 |
+
"price": 162.71,
|
| 9 |
+
"change": 1.0558350413017932,
|
| 10 |
+
"timestamp": "2025-12-07 00:21:28"
|
| 11 |
+
}
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"timestamp": "2025-12-07T00:21:28.423214",
|
| 15 |
+
"type": "MARKET",
|
| 16 |
+
"symbol": "MSFT",
|
| 17 |
+
"message": "\ud83d\udcc9 DOWN ALERT: MSFT moved -0.98% to $433.44",
|
| 18 |
+
"details": {
|
| 19 |
+
"price": 433.44,
|
| 20 |
+
"change": -0.980056199026802,
|
| 21 |
+
"timestamp": "2025-12-07 00:21:28"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"timestamp": "2025-12-07T00:21:27.968412",
|
| 26 |
+
"type": "MARKET",
|
| 27 |
+
"symbol": "NVDA",
|
| 28 |
+
"message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -0.73% to $393.71",
|
| 29 |
+
"details": {
|
| 30 |
+
"price": 393.71,
|
| 31 |
+
"change": -0.7286938981341511,
|
| 32 |
+
"timestamp": "2025-12-07 00:21:27"
|
| 33 |
+
}
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"timestamp": "2025-12-07T00:21:27.597625",
|
| 37 |
+
"type": "MARKET",
|
| 38 |
+
"symbol": "TSLA",
|
| 39 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.10% to $255.73",
|
| 40 |
+
"details": {
|
| 41 |
+
"price": 255.73,
|
| 42 |
+
"change": 2.1041284037371164,
|
| 43 |
+
"timestamp": "2025-12-07 00:21:27"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"timestamp": "2025-12-07T00:21:27.288732",
|
| 48 |
+
"type": "MARKET",
|
| 49 |
+
"symbol": "AAPL",
|
| 50 |
+
"message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.46% to $190.98",
|
| 51 |
+
"details": {
|
| 52 |
+
"price": 190.98,
|
| 53 |
+
"change": 2.4625784645099005,
|
| 54 |
+
"timestamp": "2025-12-07 00:21:27"
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"timestamp": "2025-12-07T00:21:16.724221",
|
| 59 |
+
"type": "MARKET",
|
| 60 |
+
"symbol": "AMZN",
|
| 61 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.06% to $162.71",
|
| 62 |
+
"details": {
|
| 63 |
+
"price": 162.71,
|
| 64 |
+
"change": 1.0558350413017932,
|
| 65 |
+
"timestamp": "2025-12-07 00:21:16"
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"timestamp": "2025-12-07T00:21:16.459301",
|
| 70 |
+
"type": "MARKET",
|
| 71 |
+
"symbol": "MSFT",
|
| 72 |
+
"message": "\ud83d\udcc9 DOWN ALERT: MSFT moved -0.98% to $433.44",
|
| 73 |
+
"details": {
|
| 74 |
+
"price": 433.44,
|
| 75 |
+
"change": -0.980056199026802,
|
| 76 |
+
"timestamp": "2025-12-07 00:21:16"
|
| 77 |
+
}
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"timestamp": "2025-12-07T00:21:16.216594",
|
| 81 |
+
"type": "MARKET",
|
| 82 |
+
"symbol": "NVDA",
|
| 83 |
+
"message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -0.73% to $393.71",
|
| 84 |
+
"details": {
|
| 85 |
+
"price": 393.71,
|
| 86 |
+
"change": -0.7286938981341511,
|
| 87 |
+
"timestamp": "2025-12-07 00:21:16"
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"timestamp": "2025-12-07T00:21:15.967887",
|
| 92 |
+
"type": "MARKET",
|
| 93 |
+
"symbol": "TSLA",
|
| 94 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.10% to $255.73",
|
| 95 |
+
"details": {
|
| 96 |
+
"price": 255.73,
|
| 97 |
+
"change": 2.1041284037371164,
|
| 98 |
+
"timestamp": "2025-12-07 00:21:15"
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"timestamp": "2025-12-07T00:21:15.708764",
|
| 103 |
+
"type": "MARKET",
|
| 104 |
+
"symbol": "AAPL",
|
| 105 |
+
"message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.46% to $190.98",
|
| 106 |
+
"details": {
|
| 107 |
+
"price": 190.98,
|
| 108 |
+
"change": 2.4625784645099005,
|
| 109 |
+
"timestamp": "2025-12-07 00:21:15"
|
| 110 |
+
}
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"timestamp": "2025-12-07T00:21:05.175017",
|
| 114 |
+
"type": "MARKET",
|
| 115 |
+
"symbol": "AMZN",
|
| 116 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.06% to $162.71",
|
| 117 |
+
"details": {
|
| 118 |
+
"price": 162.71,
|
| 119 |
+
"change": 1.0558350413017932,
|
| 120 |
+
"timestamp": "2025-12-07 00:21:05"
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"timestamp": "2025-12-07T00:21:04.914370",
|
| 125 |
+
"type": "MARKET",
|
| 126 |
+
"symbol": "MSFT",
|
| 127 |
+
"message": "\ud83d\udcc9 DOWN ALERT: MSFT moved -0.98% to $433.44",
|
| 128 |
+
"details": {
|
| 129 |
+
"price": 433.44,
|
| 130 |
+
"change": -0.980056199026802,
|
| 131 |
+
"timestamp": "2025-12-07 00:21:04"
|
| 132 |
+
}
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"timestamp": "2025-12-07T00:21:04.664472",
|
| 136 |
+
"type": "MARKET",
|
| 137 |
+
"symbol": "NVDA",
|
| 138 |
+
"message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -0.73% to $393.71",
|
| 139 |
+
"details": {
|
| 140 |
+
"price": 393.71,
|
| 141 |
+
"change": -0.7286938981341511,
|
| 142 |
+
"timestamp": "2025-12-07 00:21:04"
|
| 143 |
+
}
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"timestamp": "2025-12-07T00:21:04.416758",
|
| 147 |
+
"type": "MARKET",
|
| 148 |
+
"symbol": "TSLA",
|
| 149 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.10% to $255.73",
|
| 150 |
+
"details": {
|
| 151 |
+
"price": 255.73,
|
| 152 |
+
"change": 2.1041284037371164,
|
| 153 |
+
"timestamp": "2025-12-07 00:21:04"
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"timestamp": "2025-12-07T00:21:04.139567",
|
| 158 |
+
"type": "MARKET",
|
| 159 |
+
"symbol": "AAPL",
|
| 160 |
+
"message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.46% to $190.98",
|
| 161 |
+
"details": {
|
| 162 |
+
"price": 190.98,
|
| 163 |
+
"change": 2.4625784645099005,
|
| 164 |
+
"timestamp": "2025-12-07 00:21:04"
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"timestamp": "2025-12-07T00:20:53.847628",
|
| 169 |
+
"type": "MARKET",
|
| 170 |
+
"symbol": "GOOGL",
|
| 171 |
+
"message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
|
| 172 |
+
"details": {
|
| 173 |
+
"price": 171.36,
|
| 174 |
+
"change": -0.8103727714748652,
|
| 175 |
+
"timestamp": "2025-12-07 00:20:53"
|
| 176 |
+
}
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"timestamp": "2025-12-07T00:20:53.592544",
|
| 180 |
+
"type": "MARKET",
|
| 181 |
+
"symbol": "AMZN",
|
| 182 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
|
| 183 |
+
"details": {
|
| 184 |
+
"price": 184.5,
|
| 185 |
+
"change": -0.6033832561146453,
|
| 186 |
+
"timestamp": "2025-12-07 00:20:53"
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"timestamp": "2025-12-07T00:20:53.337153",
|
| 191 |
+
"type": "MARKET",
|
| 192 |
+
"symbol": "MSFT",
|
| 193 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
|
| 194 |
+
"details": {
|
| 195 |
+
"price": 425.31,
|
| 196 |
+
"change": 1.8170066072967477,
|
| 197 |
+
"timestamp": "2025-12-07 00:20:53"
|
| 198 |
+
}
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"timestamp": "2025-12-07T00:20:53.050481",
|
| 202 |
+
"type": "MARKET",
|
| 203 |
+
"symbol": "NVDA",
|
| 204 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
|
| 205 |
+
"details": {
|
| 206 |
+
"price": 488.34,
|
| 207 |
+
"change": 1.782029637966601,
|
| 208 |
+
"timestamp": "2025-12-07 00:20:53"
|
| 209 |
+
}
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"timestamp": "2025-12-07T00:20:52.449885",
|
| 213 |
+
"type": "MARKET",
|
| 214 |
+
"symbol": "AAPL",
|
| 215 |
+
"message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
|
| 216 |
+
"details": {
|
| 217 |
+
"price": 136.77,
|
| 218 |
+
"change": 2.029093621782916,
|
| 219 |
+
"timestamp": "2025-12-07 00:20:52"
|
| 220 |
+
}
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"timestamp": "2025-12-07T00:20:42.155953",
|
| 224 |
+
"type": "MARKET",
|
| 225 |
+
"symbol": "GOOGL",
|
| 226 |
+
"message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
|
| 227 |
+
"details": {
|
| 228 |
+
"price": 171.36,
|
| 229 |
+
"change": -0.8103727714748652,
|
| 230 |
+
"timestamp": "2025-12-07 00:20:42"
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"timestamp": "2025-12-07T00:20:41.898578",
|
| 235 |
+
"type": "MARKET",
|
| 236 |
+
"symbol": "AMZN",
|
| 237 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
|
| 238 |
+
"details": {
|
| 239 |
+
"price": 184.5,
|
| 240 |
+
"change": -0.6033832561146453,
|
| 241 |
+
"timestamp": "2025-12-07 00:20:41"
|
| 242 |
+
}
|
| 243 |
+
},
|
| 244 |
+
{
|
| 245 |
+
"timestamp": "2025-12-07T00:20:41.604483",
|
| 246 |
+
"type": "MARKET",
|
| 247 |
+
"symbol": "MSFT",
|
| 248 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
|
| 249 |
+
"details": {
|
| 250 |
+
"price": 425.31,
|
| 251 |
+
"change": 1.8170066072967477,
|
| 252 |
+
"timestamp": "2025-12-07 00:20:41"
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"timestamp": "2025-12-07T00:20:41.354291",
|
| 257 |
+
"type": "MARKET",
|
| 258 |
+
"symbol": "NVDA",
|
| 259 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
|
| 260 |
+
"details": {
|
| 261 |
+
"price": 488.34,
|
| 262 |
+
"change": 1.782029637966601,
|
| 263 |
+
"timestamp": "2025-12-07 00:20:41"
|
| 264 |
+
}
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"timestamp": "2025-12-07T00:20:40.851188",
|
| 268 |
+
"type": "MARKET",
|
| 269 |
+
"symbol": "AAPL",
|
| 270 |
+
"message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
|
| 271 |
+
"details": {
|
| 272 |
+
"price": 136.77,
|
| 273 |
+
"change": 2.029093621782916,
|
| 274 |
+
"timestamp": "2025-12-07 00:20:40"
|
| 275 |
+
}
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"timestamp": "2025-12-07T00:20:30.572421",
|
| 279 |
+
"type": "MARKET",
|
| 280 |
+
"symbol": "GOOGL",
|
| 281 |
+
"message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
|
| 282 |
+
"details": {
|
| 283 |
+
"price": 171.36,
|
| 284 |
+
"change": -0.8103727714748652,
|
| 285 |
+
"timestamp": "2025-12-07 00:20:30"
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"timestamp": "2025-12-07T00:20:30.309398",
|
| 290 |
+
"type": "MARKET",
|
| 291 |
+
"symbol": "AMZN",
|
| 292 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
|
| 293 |
+
"details": {
|
| 294 |
+
"price": 184.5,
|
| 295 |
+
"change": -0.6033832561146453,
|
| 296 |
+
"timestamp": "2025-12-07 00:20:30"
|
| 297 |
+
}
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"timestamp": "2025-12-07T00:20:30.035706",
|
| 301 |
+
"type": "MARKET",
|
| 302 |
+
"symbol": "MSFT",
|
| 303 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
|
| 304 |
+
"details": {
|
| 305 |
+
"price": 425.31,
|
| 306 |
+
"change": 1.8170066072967477,
|
| 307 |
+
"timestamp": "2025-12-07 00:20:30"
|
| 308 |
+
}
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
"timestamp": "2025-12-07T00:20:29.787691",
|
| 312 |
+
"type": "MARKET",
|
| 313 |
+
"symbol": "NVDA",
|
| 314 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
|
| 315 |
+
"details": {
|
| 316 |
+
"price": 488.34,
|
| 317 |
+
"change": 1.782029637966601,
|
| 318 |
+
"timestamp": "2025-12-07 00:20:29"
|
| 319 |
+
}
|
| 320 |
+
},
|
| 321 |
+
{
|
| 322 |
+
"timestamp": "2025-12-07T00:20:29.292529",
|
| 323 |
+
"type": "MARKET",
|
| 324 |
+
"symbol": "AAPL",
|
| 325 |
+
"message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
|
| 326 |
+
"details": {
|
| 327 |
+
"price": 136.77,
|
| 328 |
+
"change": 2.029093621782916,
|
| 329 |
+
"timestamp": "2025-12-07 00:20:29"
|
| 330 |
+
}
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"timestamp": "2025-12-07T00:20:18.985643",
|
| 334 |
+
"type": "MARKET",
|
| 335 |
+
"symbol": "GOOGL",
|
| 336 |
+
"message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
|
| 337 |
+
"details": {
|
| 338 |
+
"price": 171.36,
|
| 339 |
+
"change": -0.8103727714748652,
|
| 340 |
+
"timestamp": "2025-12-07 00:20:18"
|
| 341 |
+
}
|
| 342 |
+
},
|
| 343 |
+
{
|
| 344 |
+
"timestamp": "2025-12-07T00:20:18.706961",
|
| 345 |
+
"type": "MARKET",
|
| 346 |
+
"symbol": "AMZN",
|
| 347 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
|
| 348 |
+
"details": {
|
| 349 |
+
"price": 184.5,
|
| 350 |
+
"change": -0.6033832561146453,
|
| 351 |
+
"timestamp": "2025-12-07 00:20:18"
|
| 352 |
+
}
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
"timestamp": "2025-12-07T00:20:18.452874",
|
| 356 |
+
"type": "MARKET",
|
| 357 |
+
"symbol": "MSFT",
|
| 358 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
|
| 359 |
+
"details": {
|
| 360 |
+
"price": 425.31,
|
| 361 |
+
"change": 1.8170066072967477,
|
| 362 |
+
"timestamp": "2025-12-07 00:20:18"
|
| 363 |
+
}
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"timestamp": "2025-12-07T00:20:18.179318",
|
| 367 |
+
"type": "MARKET",
|
| 368 |
+
"symbol": "NVDA",
|
| 369 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
|
| 370 |
+
"details": {
|
| 371 |
+
"price": 488.34,
|
| 372 |
+
"change": 1.782029637966601,
|
| 373 |
+
"timestamp": "2025-12-07 00:20:18"
|
| 374 |
+
}
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"timestamp": "2025-12-07T00:20:17.316521",
|
| 378 |
+
"type": "MARKET",
|
| 379 |
+
"symbol": "AAPL",
|
| 380 |
+
"message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
|
| 381 |
+
"details": {
|
| 382 |
+
"price": 136.77,
|
| 383 |
+
"change": 2.029093621782916,
|
| 384 |
+
"timestamp": "2025-12-07 00:20:17"
|
| 385 |
+
}
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"timestamp": "2025-12-07T00:20:07.062668",
|
| 389 |
+
"type": "MARKET",
|
| 390 |
+
"symbol": "GOOGL",
|
| 391 |
+
"message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
|
| 392 |
+
"details": {
|
| 393 |
+
"price": 171.36,
|
| 394 |
+
"change": -0.8103727714748652,
|
| 395 |
+
"timestamp": "2025-12-07 00:20:07"
|
| 396 |
+
}
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"timestamp": "2025-12-07T00:20:06.813052",
|
| 400 |
+
"type": "MARKET",
|
| 401 |
+
"symbol": "AMZN",
|
| 402 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
|
| 403 |
+
"details": {
|
| 404 |
+
"price": 184.5,
|
| 405 |
+
"change": -0.6033832561146453,
|
| 406 |
+
"timestamp": "2025-12-07 00:20:06"
|
| 407 |
+
}
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"timestamp": "2025-12-07T00:20:06.564151",
|
| 411 |
+
"type": "MARKET",
|
| 412 |
+
"symbol": "MSFT",
|
| 413 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
|
| 414 |
+
"details": {
|
| 415 |
+
"price": 425.31,
|
| 416 |
+
"change": 1.8170066072967477,
|
| 417 |
+
"timestamp": "2025-12-07 00:20:06"
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
"timestamp": "2025-12-07T00:20:06.277580",
|
| 422 |
+
"type": "MARKET",
|
| 423 |
+
"symbol": "NVDA",
|
| 424 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
|
| 425 |
+
"details": {
|
| 426 |
+
"price": 488.34,
|
| 427 |
+
"change": 1.782029637966601,
|
| 428 |
+
"timestamp": "2025-12-07 00:20:06"
|
| 429 |
+
}
|
| 430 |
+
},
|
| 431 |
+
{
|
| 432 |
+
"timestamp": "2025-12-07T00:20:05.730732",
|
| 433 |
+
"type": "MARKET",
|
| 434 |
+
"symbol": "AAPL",
|
| 435 |
+
"message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
|
| 436 |
+
"details": {
|
| 437 |
+
"price": 136.77,
|
| 438 |
+
"change": 2.029093621782916,
|
| 439 |
+
"timestamp": "2025-12-07 00:20:05"
|
| 440 |
+
}
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"timestamp": "2025-12-07T00:19:55.457222",
|
| 444 |
+
"type": "MARKET",
|
| 445 |
+
"symbol": "GOOGL",
|
| 446 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
|
| 447 |
+
"details": {
|
| 448 |
+
"price": 161.03,
|
| 449 |
+
"change": 2.3257291732858847,
|
| 450 |
+
"timestamp": "2025-12-07 00:19:55"
|
| 451 |
+
}
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"timestamp": "2025-12-07T00:19:54.959505",
|
| 455 |
+
"type": "MARKET",
|
| 456 |
+
"symbol": "MSFT",
|
| 457 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
|
| 458 |
+
"details": {
|
| 459 |
+
"price": 388.37,
|
| 460 |
+
"change": 4.574829016102108,
|
| 461 |
+
"timestamp": "2025-12-07 00:19:54"
|
| 462 |
+
}
|
| 463 |
+
},
|
| 464 |
+
{
|
| 465 |
+
"timestamp": "2025-12-07T00:19:54.706628",
|
| 466 |
+
"type": "MARKET",
|
| 467 |
+
"symbol": "NVDA",
|
| 468 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
|
| 469 |
+
"details": {
|
| 470 |
+
"price": 417.47,
|
| 471 |
+
"change": 1.5470312081924578,
|
| 472 |
+
"timestamp": "2025-12-07 00:19:54"
|
| 473 |
+
}
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"timestamp": "2025-12-07T00:19:54.444375",
|
| 477 |
+
"type": "MARKET",
|
| 478 |
+
"symbol": "TSLA",
|
| 479 |
+
"message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
|
| 480 |
+
"details": {
|
| 481 |
+
"price": 232.45,
|
| 482 |
+
"change": -0.6666381778556483,
|
| 483 |
+
"timestamp": "2025-12-07 00:19:54"
|
| 484 |
+
}
|
| 485 |
+
},
|
| 486 |
+
{
|
| 487 |
+
"timestamp": "2025-12-07T00:19:54.175770",
|
| 488 |
+
"type": "MARKET",
|
| 489 |
+
"symbol": "AAPL",
|
| 490 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
|
| 491 |
+
"details": {
|
| 492 |
+
"price": 174.63,
|
| 493 |
+
"change": -0.5297334244702704,
|
| 494 |
+
"timestamp": "2025-12-07 00:19:54"
|
| 495 |
+
}
|
| 496 |
+
},
|
| 497 |
+
{
|
| 498 |
+
"timestamp": "2025-12-07T00:19:43.879355",
|
| 499 |
+
"type": "MARKET",
|
| 500 |
+
"symbol": "GOOGL",
|
| 501 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
|
| 502 |
+
"details": {
|
| 503 |
+
"price": 161.03,
|
| 504 |
+
"change": 2.3257291732858847,
|
| 505 |
+
"timestamp": "2025-12-07 00:19:43"
|
| 506 |
+
}
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"timestamp": "2025-12-07T00:19:43.353084",
|
| 510 |
+
"type": "MARKET",
|
| 511 |
+
"symbol": "MSFT",
|
| 512 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
|
| 513 |
+
"details": {
|
| 514 |
+
"price": 388.37,
|
| 515 |
+
"change": 4.574829016102108,
|
| 516 |
+
"timestamp": "2025-12-07 00:19:43"
|
| 517 |
+
}
|
| 518 |
+
},
|
| 519 |
+
{
|
| 520 |
+
"timestamp": "2025-12-07T00:19:43.108885",
|
| 521 |
+
"type": "MARKET",
|
| 522 |
+
"symbol": "NVDA",
|
| 523 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
|
| 524 |
+
"details": {
|
| 525 |
+
"price": 417.47,
|
| 526 |
+
"change": 1.5470312081924578,
|
| 527 |
+
"timestamp": "2025-12-07 00:19:43"
|
| 528 |
+
}
|
| 529 |
+
},
|
| 530 |
+
{
|
| 531 |
+
"timestamp": "2025-12-07T00:19:42.833784",
|
| 532 |
+
"type": "MARKET",
|
| 533 |
+
"symbol": "TSLA",
|
| 534 |
+
"message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
|
| 535 |
+
"details": {
|
| 536 |
+
"price": 232.45,
|
| 537 |
+
"change": -0.6666381778556483,
|
| 538 |
+
"timestamp": "2025-12-07 00:19:42"
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"timestamp": "2025-12-07T00:19:42.562678",
|
| 543 |
+
"type": "MARKET",
|
| 544 |
+
"symbol": "AAPL",
|
| 545 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
|
| 546 |
+
"details": {
|
| 547 |
+
"price": 174.63,
|
| 548 |
+
"change": -0.5297334244702704,
|
| 549 |
+
"timestamp": "2025-12-07 00:19:42"
|
| 550 |
+
}
|
| 551 |
+
},
|
| 552 |
+
{
|
| 553 |
+
"timestamp": "2025-12-07T00:19:32.270914",
|
| 554 |
+
"type": "MARKET",
|
| 555 |
+
"symbol": "GOOGL",
|
| 556 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
|
| 557 |
+
"details": {
|
| 558 |
+
"price": 161.03,
|
| 559 |
+
"change": 2.3257291732858847,
|
| 560 |
+
"timestamp": "2025-12-07 00:19:32"
|
| 561 |
+
}
|
| 562 |
+
},
|
| 563 |
+
{
|
| 564 |
+
"timestamp": "2025-12-07T00:19:31.741726",
|
| 565 |
+
"type": "MARKET",
|
| 566 |
+
"symbol": "MSFT",
|
| 567 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
|
| 568 |
+
"details": {
|
| 569 |
+
"price": 388.37,
|
| 570 |
+
"change": 4.574829016102108,
|
| 571 |
+
"timestamp": "2025-12-07 00:19:31"
|
| 572 |
+
}
|
| 573 |
+
},
|
| 574 |
+
{
|
| 575 |
+
"timestamp": "2025-12-07T00:19:31.484246",
|
| 576 |
+
"type": "MARKET",
|
| 577 |
+
"symbol": "NVDA",
|
| 578 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
|
| 579 |
+
"details": {
|
| 580 |
+
"price": 417.47,
|
| 581 |
+
"change": 1.5470312081924578,
|
| 582 |
+
"timestamp": "2025-12-07 00:19:31"
|
| 583 |
+
}
|
| 584 |
+
},
|
| 585 |
+
{
|
| 586 |
+
"timestamp": "2025-12-07T00:19:31.235216",
|
| 587 |
+
"type": "MARKET",
|
| 588 |
+
"symbol": "TSLA",
|
| 589 |
+
"message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
|
| 590 |
+
"details": {
|
| 591 |
+
"price": 232.45,
|
| 592 |
+
"change": -0.6666381778556483,
|
| 593 |
+
"timestamp": "2025-12-07 00:19:31"
|
| 594 |
+
}
|
| 595 |
+
},
|
| 596 |
+
{
|
| 597 |
+
"timestamp": "2025-12-07T00:19:30.943159",
|
| 598 |
+
"type": "MARKET",
|
| 599 |
+
"symbol": "AAPL",
|
| 600 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
|
| 601 |
+
"details": {
|
| 602 |
+
"price": 174.63,
|
| 603 |
+
"change": -0.5297334244702704,
|
| 604 |
+
"timestamp": "2025-12-07 00:19:30"
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
{
|
| 608 |
+
"timestamp": "2025-12-07T00:19:20.683026",
|
| 609 |
+
"type": "MARKET",
|
| 610 |
+
"symbol": "GOOGL",
|
| 611 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
|
| 612 |
+
"details": {
|
| 613 |
+
"price": 161.03,
|
| 614 |
+
"change": 2.3257291732858847,
|
| 615 |
+
"timestamp": "2025-12-07 00:19:20"
|
| 616 |
+
}
|
| 617 |
+
},
|
| 618 |
+
{
|
| 619 |
+
"timestamp": "2025-12-07T00:19:20.171274",
|
| 620 |
+
"type": "MARKET",
|
| 621 |
+
"symbol": "MSFT",
|
| 622 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
|
| 623 |
+
"details": {
|
| 624 |
+
"price": 388.37,
|
| 625 |
+
"change": 4.574829016102108,
|
| 626 |
+
"timestamp": "2025-12-07 00:19:20"
|
| 627 |
+
}
|
| 628 |
+
},
|
| 629 |
+
{
|
| 630 |
+
"timestamp": "2025-12-07T00:19:19.925430",
|
| 631 |
+
"type": "MARKET",
|
| 632 |
+
"symbol": "NVDA",
|
| 633 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
|
| 634 |
+
"details": {
|
| 635 |
+
"price": 417.47,
|
| 636 |
+
"change": 1.5470312081924578,
|
| 637 |
+
"timestamp": "2025-12-07 00:19:19"
|
| 638 |
+
}
|
| 639 |
+
},
|
| 640 |
+
{
|
| 641 |
+
"timestamp": "2025-12-07T00:19:19.667863",
|
| 642 |
+
"type": "MARKET",
|
| 643 |
+
"symbol": "TSLA",
|
| 644 |
+
"message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
|
| 645 |
+
"details": {
|
| 646 |
+
"price": 232.45,
|
| 647 |
+
"change": -0.6666381778556483,
|
| 648 |
+
"timestamp": "2025-12-07 00:19:19"
|
| 649 |
+
}
|
| 650 |
+
},
|
| 651 |
+
{
|
| 652 |
+
"timestamp": "2025-12-07T00:19:19.367448",
|
| 653 |
+
"type": "MARKET",
|
| 654 |
+
"symbol": "AAPL",
|
| 655 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
|
| 656 |
+
"details": {
|
| 657 |
+
"price": 174.63,
|
| 658 |
+
"change": -0.5297334244702704,
|
| 659 |
+
"timestamp": "2025-12-07 00:19:19"
|
| 660 |
+
}
|
| 661 |
+
},
|
| 662 |
+
{
|
| 663 |
+
"timestamp": "2025-12-07T00:19:09.080842",
|
| 664 |
+
"type": "MARKET",
|
| 665 |
+
"symbol": "GOOGL",
|
| 666 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
|
| 667 |
+
"details": {
|
| 668 |
+
"price": 161.03,
|
| 669 |
+
"change": 2.3257291732858847,
|
| 670 |
+
"timestamp": "2025-12-07 00:19:09"
|
| 671 |
+
}
|
| 672 |
+
},
|
| 673 |
+
{
|
| 674 |
+
"timestamp": "2025-12-07T00:19:08.583771",
|
| 675 |
+
"type": "MARKET",
|
| 676 |
+
"symbol": "MSFT",
|
| 677 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
|
| 678 |
+
"details": {
|
| 679 |
+
"price": 388.37,
|
| 680 |
+
"change": 4.574829016102108,
|
| 681 |
+
"timestamp": "2025-12-07 00:19:08"
|
| 682 |
+
}
|
| 683 |
+
},
|
| 684 |
+
{
|
| 685 |
+
"timestamp": "2025-12-07T00:19:08.356347",
|
| 686 |
+
"type": "MARKET",
|
| 687 |
+
"symbol": "NVDA",
|
| 688 |
+
"message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
|
| 689 |
+
"details": {
|
| 690 |
+
"price": 417.47,
|
| 691 |
+
"change": 1.5470312081924578,
|
| 692 |
+
"timestamp": "2025-12-07 00:19:08"
|
| 693 |
+
}
|
| 694 |
+
},
|
| 695 |
+
{
|
| 696 |
+
"timestamp": "2025-12-07T00:19:08.100527",
|
| 697 |
+
"type": "MARKET",
|
| 698 |
+
"symbol": "TSLA",
|
| 699 |
+
"message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
|
| 700 |
+
"details": {
|
| 701 |
+
"price": 232.45,
|
| 702 |
+
"change": -0.6666381778556483,
|
| 703 |
+
"timestamp": "2025-12-07 00:19:08"
|
| 704 |
+
}
|
| 705 |
+
},
|
| 706 |
+
{
|
| 707 |
+
"timestamp": "2025-12-07T00:19:07.845662",
|
| 708 |
+
"type": "MARKET",
|
| 709 |
+
"symbol": "AAPL",
|
| 710 |
+
"message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
|
| 711 |
+
"details": {
|
| 712 |
+
"price": 174.63,
|
| 713 |
+
"change": -0.5297334244702704,
|
| 714 |
+
"timestamp": "2025-12-07 00:19:07"
|
| 715 |
+
}
|
| 716 |
+
},
|
| 717 |
+
{
|
| 718 |
+
"timestamp": "2025-12-07T00:18:57.572888",
|
| 719 |
+
"type": "MARKET",
|
| 720 |
+
"symbol": "GOOGL",
|
| 721 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
|
| 722 |
+
"details": {
|
| 723 |
+
"price": 154.7,
|
| 724 |
+
"change": 5.893627216099654,
|
| 725 |
+
"timestamp": "2025-12-07 00:18:57"
|
| 726 |
+
}
|
| 727 |
+
},
|
| 728 |
+
{
|
| 729 |
+
"timestamp": "2025-12-07T00:18:57.320075",
|
| 730 |
+
"type": "MARKET",
|
| 731 |
+
"symbol": "AMZN",
|
| 732 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
|
| 733 |
+
"details": {
|
| 734 |
+
"price": 152.36,
|
| 735 |
+
"change": 2.620057924159773,
|
| 736 |
+
"timestamp": "2025-12-07 00:18:57"
|
| 737 |
+
}
|
| 738 |
+
},
|
| 739 |
+
{
|
| 740 |
+
"timestamp": "2025-12-07T00:18:57.060315",
|
| 741 |
+
"type": "MARKET",
|
| 742 |
+
"symbol": "MSFT",
|
| 743 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
|
| 744 |
+
"details": {
|
| 745 |
+
"price": 490.81,
|
| 746 |
+
"change": 2.6842127285660453,
|
| 747 |
+
"timestamp": "2025-12-07 00:18:57"
|
| 748 |
+
}
|
| 749 |
+
},
|
| 750 |
+
{
|
| 751 |
+
"timestamp": "2025-12-07T00:18:56.597685",
|
| 752 |
+
"type": "MARKET",
|
| 753 |
+
"symbol": "TSLA",
|
| 754 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
|
| 755 |
+
"details": {
|
| 756 |
+
"price": 295.24,
|
| 757 |
+
"change": 1.0507581202724416,
|
| 758 |
+
"timestamp": "2025-12-07 00:18:56"
|
| 759 |
+
}
|
| 760 |
+
},
|
| 761 |
+
{
|
| 762 |
+
"timestamp": "2025-12-07T00:18:46.026021",
|
| 763 |
+
"type": "MARKET",
|
| 764 |
+
"symbol": "GOOGL",
|
| 765 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
|
| 766 |
+
"details": {
|
| 767 |
+
"price": 154.7,
|
| 768 |
+
"change": 5.893627216099654,
|
| 769 |
+
"timestamp": "2025-12-07 00:18:46"
|
| 770 |
+
}
|
| 771 |
+
},
|
| 772 |
+
{
|
| 773 |
+
"timestamp": "2025-12-07T00:18:45.794249",
|
| 774 |
+
"type": "MARKET",
|
| 775 |
+
"symbol": "AMZN",
|
| 776 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
|
| 777 |
+
"details": {
|
| 778 |
+
"price": 152.36,
|
| 779 |
+
"change": 2.620057924159773,
|
| 780 |
+
"timestamp": "2025-12-07 00:18:45"
|
| 781 |
+
}
|
| 782 |
+
},
|
| 783 |
+
{
|
| 784 |
+
"timestamp": "2025-12-07T00:18:45.537097",
|
| 785 |
+
"type": "MARKET",
|
| 786 |
+
"symbol": "MSFT",
|
| 787 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
|
| 788 |
+
"details": {
|
| 789 |
+
"price": 490.81,
|
| 790 |
+
"change": 2.6842127285660453,
|
| 791 |
+
"timestamp": "2025-12-07 00:18:45"
|
| 792 |
+
}
|
| 793 |
+
},
|
| 794 |
+
{
|
| 795 |
+
"timestamp": "2025-12-07T00:18:45.050498",
|
| 796 |
+
"type": "MARKET",
|
| 797 |
+
"symbol": "TSLA",
|
| 798 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
|
| 799 |
+
"details": {
|
| 800 |
+
"price": 295.24,
|
| 801 |
+
"change": 1.0507581202724416,
|
| 802 |
+
"timestamp": "2025-12-07 00:18:45"
|
| 803 |
+
}
|
| 804 |
+
},
|
| 805 |
+
{
|
| 806 |
+
"timestamp": "2025-12-07T00:18:34.509053",
|
| 807 |
+
"type": "MARKET",
|
| 808 |
+
"symbol": "GOOGL",
|
| 809 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
|
| 810 |
+
"details": {
|
| 811 |
+
"price": 154.7,
|
| 812 |
+
"change": 5.893627216099654,
|
| 813 |
+
"timestamp": "2025-12-07 00:18:34"
|
| 814 |
+
}
|
| 815 |
+
},
|
| 816 |
+
{
|
| 817 |
+
"timestamp": "2025-12-07T00:18:34.158647",
|
| 818 |
+
"type": "MARKET",
|
| 819 |
+
"symbol": "AMZN",
|
| 820 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
|
| 821 |
+
"details": {
|
| 822 |
+
"price": 152.36,
|
| 823 |
+
"change": 2.620057924159773,
|
| 824 |
+
"timestamp": "2025-12-07 00:18:34"
|
| 825 |
+
}
|
| 826 |
+
},
|
| 827 |
+
{
|
| 828 |
+
"timestamp": "2025-12-07T00:18:33.893424",
|
| 829 |
+
"type": "MARKET",
|
| 830 |
+
"symbol": "MSFT",
|
| 831 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
|
| 832 |
+
"details": {
|
| 833 |
+
"price": 490.81,
|
| 834 |
+
"change": 2.6842127285660453,
|
| 835 |
+
"timestamp": "2025-12-07 00:18:33"
|
| 836 |
+
}
|
| 837 |
+
},
|
| 838 |
+
{
|
| 839 |
+
"timestamp": "2025-12-07T00:18:33.330802",
|
| 840 |
+
"type": "MARKET",
|
| 841 |
+
"symbol": "TSLA",
|
| 842 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
|
| 843 |
+
"details": {
|
| 844 |
+
"price": 295.24,
|
| 845 |
+
"change": 1.0507581202724416,
|
| 846 |
+
"timestamp": "2025-12-07 00:18:33"
|
| 847 |
+
}
|
| 848 |
+
},
|
| 849 |
+
{
|
| 850 |
+
"timestamp": "2025-12-07T00:18:22.729714",
|
| 851 |
+
"type": "MARKET",
|
| 852 |
+
"symbol": "GOOGL",
|
| 853 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
|
| 854 |
+
"details": {
|
| 855 |
+
"price": 154.7,
|
| 856 |
+
"change": 5.893627216099654,
|
| 857 |
+
"timestamp": "2025-12-07 00:18:22"
|
| 858 |
+
}
|
| 859 |
+
},
|
| 860 |
+
{
|
| 861 |
+
"timestamp": "2025-12-07T00:18:22.468028",
|
| 862 |
+
"type": "MARKET",
|
| 863 |
+
"symbol": "AMZN",
|
| 864 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
|
| 865 |
+
"details": {
|
| 866 |
+
"price": 152.36,
|
| 867 |
+
"change": 2.620057924159773,
|
| 868 |
+
"timestamp": "2025-12-07 00:18:22"
|
| 869 |
+
}
|
| 870 |
+
},
|
| 871 |
+
{
|
| 872 |
+
"timestamp": "2025-12-07T00:18:22.212773",
|
| 873 |
+
"type": "MARKET",
|
| 874 |
+
"symbol": "MSFT",
|
| 875 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
|
| 876 |
+
"details": {
|
| 877 |
+
"price": 490.81,
|
| 878 |
+
"change": 2.6842127285660453,
|
| 879 |
+
"timestamp": "2025-12-07 00:18:22"
|
| 880 |
+
}
|
| 881 |
+
},
|
| 882 |
+
{
|
| 883 |
+
"timestamp": "2025-12-07T00:18:21.714340",
|
| 884 |
+
"type": "MARKET",
|
| 885 |
+
"symbol": "TSLA",
|
| 886 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
|
| 887 |
+
"details": {
|
| 888 |
+
"price": 295.24,
|
| 889 |
+
"change": 1.0507581202724416,
|
| 890 |
+
"timestamp": "2025-12-07 00:18:21"
|
| 891 |
+
}
|
| 892 |
+
},
|
| 893 |
+
{
|
| 894 |
+
"timestamp": "2025-12-07T00:18:11.153845",
|
| 895 |
+
"type": "MARKET",
|
| 896 |
+
"symbol": "GOOGL",
|
| 897 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
|
| 898 |
+
"details": {
|
| 899 |
+
"price": 154.7,
|
| 900 |
+
"change": 5.893627216099654,
|
| 901 |
+
"timestamp": "2025-12-07 00:18:11"
|
| 902 |
+
}
|
| 903 |
+
},
|
| 904 |
+
{
|
| 905 |
+
"timestamp": "2025-12-07T00:18:10.897046",
|
| 906 |
+
"type": "MARKET",
|
| 907 |
+
"symbol": "AMZN",
|
| 908 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
|
| 909 |
+
"details": {
|
| 910 |
+
"price": 152.36,
|
| 911 |
+
"change": 2.620057924159773,
|
| 912 |
+
"timestamp": "2025-12-07 00:18:10"
|
| 913 |
+
}
|
| 914 |
+
},
|
| 915 |
+
{
|
| 916 |
+
"timestamp": "2025-12-07T00:18:10.638176",
|
| 917 |
+
"type": "MARKET",
|
| 918 |
+
"symbol": "MSFT",
|
| 919 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
|
| 920 |
+
"details": {
|
| 921 |
+
"price": 490.81,
|
| 922 |
+
"change": 2.6842127285660453,
|
| 923 |
+
"timestamp": "2025-12-07 00:18:10"
|
| 924 |
+
}
|
| 925 |
+
},
|
| 926 |
+
{
|
| 927 |
+
"timestamp": "2025-12-07T00:18:10.065308",
|
| 928 |
+
"type": "MARKET",
|
| 929 |
+
"symbol": "TSLA",
|
| 930 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
|
| 931 |
+
"details": {
|
| 932 |
+
"price": 295.24,
|
| 933 |
+
"change": 1.0507581202724416,
|
| 934 |
+
"timestamp": "2025-12-07 00:18:10"
|
| 935 |
+
}
|
| 936 |
+
},
|
| 937 |
+
{
|
| 938 |
+
"timestamp": "2025-12-07T00:17:59.312775",
|
| 939 |
+
"type": "MARKET",
|
| 940 |
+
"symbol": "GOOGL",
|
| 941 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +0.86% to $147.04",
|
| 942 |
+
"details": {
|
| 943 |
+
"price": 147.04,
|
| 944 |
+
"change": 0.8573976267233693,
|
| 945 |
+
"timestamp": "2025-12-07 00:17:59"
|
| 946 |
+
}
|
| 947 |
+
},
|
| 948 |
+
{
|
| 949 |
+
"timestamp": "2025-12-07T00:17:59.073640",
|
| 950 |
+
"type": "MARKET",
|
| 951 |
+
"symbol": "AMZN",
|
| 952 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.69% to $181.88",
|
| 953 |
+
"details": {
|
| 954 |
+
"price": 181.88,
|
| 955 |
+
"change": 1.6941571149007555,
|
| 956 |
+
"timestamp": "2025-12-07 00:17:59"
|
| 957 |
+
}
|
| 958 |
+
},
|
| 959 |
+
{
|
| 960 |
+
"timestamp": "2025-12-07T00:17:58.765582",
|
| 961 |
+
"type": "MARKET",
|
| 962 |
+
"symbol": "MSFT",
|
| 963 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +3.55% to $417.01",
|
| 964 |
+
"details": {
|
| 965 |
+
"price": 417.01,
|
| 966 |
+
"change": 3.5457999155761857,
|
| 967 |
+
"timestamp": "2025-12-07 00:17:58"
|
| 968 |
+
}
|
| 969 |
+
},
|
| 970 |
+
{
|
| 971 |
+
"timestamp": "2025-12-07T00:17:58.535908",
|
| 972 |
+
"type": "MARKET",
|
| 973 |
+
"symbol": "NVDA",
|
| 974 |
+
"message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -2.75% to $352.00",
|
| 975 |
+
"details": {
|
| 976 |
+
"price": 352.0,
|
| 977 |
+
"change": -2.748998480453098,
|
| 978 |
+
"timestamp": "2025-12-07 00:17:58"
|
| 979 |
+
}
|
| 980 |
+
},
|
| 981 |
+
{
|
| 982 |
+
"timestamp": "2025-12-07T00:17:58.135874",
|
| 983 |
+
"type": "MARKET",
|
| 984 |
+
"symbol": "TSLA",
|
| 985 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.70% to $293.18",
|
| 986 |
+
"details": {
|
| 987 |
+
"price": 293.18,
|
| 988 |
+
"change": 2.7044069221607328,
|
| 989 |
+
"timestamp": "2025-12-07 00:17:57"
|
| 990 |
+
}
|
| 991 |
+
},
|
| 992 |
+
{
|
| 993 |
+
"timestamp": "2025-12-07T00:17:45.561204",
|
| 994 |
+
"type": "MARKET",
|
| 995 |
+
"symbol": "GOOGL",
|
| 996 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +0.86% to $147.04",
|
| 997 |
+
"details": {
|
| 998 |
+
"price": 147.04,
|
| 999 |
+
"change": 0.8573976267233693,
|
| 1000 |
+
"timestamp": "2025-12-07 00:17:45"
|
| 1001 |
+
}
|
| 1002 |
+
},
|
| 1003 |
+
{
|
| 1004 |
+
"timestamp": "2025-12-07T00:17:45.294768",
|
| 1005 |
+
"type": "MARKET",
|
| 1006 |
+
"symbol": "AMZN",
|
| 1007 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.69% to $181.88",
|
| 1008 |
+
"details": {
|
| 1009 |
+
"price": 181.88,
|
| 1010 |
+
"change": 1.6941571149007555,
|
| 1011 |
+
"timestamp": "2025-12-07 00:17:45"
|
| 1012 |
+
}
|
| 1013 |
+
},
|
| 1014 |
+
{
|
| 1015 |
+
"timestamp": "2025-12-07T00:17:45.029167",
|
| 1016 |
+
"type": "MARKET",
|
| 1017 |
+
"symbol": "MSFT",
|
| 1018 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +3.55% to $417.01",
|
| 1019 |
+
"details": {
|
| 1020 |
+
"price": 417.01,
|
| 1021 |
+
"change": 3.5457999155761857,
|
| 1022 |
+
"timestamp": "2025-12-07 00:17:45"
|
| 1023 |
+
}
|
| 1024 |
+
},
|
| 1025 |
+
{
|
| 1026 |
+
"timestamp": "2025-12-07T00:17:44.757997",
|
| 1027 |
+
"type": "MARKET",
|
| 1028 |
+
"symbol": "NVDA",
|
| 1029 |
+
"message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -2.75% to $352.00",
|
| 1030 |
+
"details": {
|
| 1031 |
+
"price": 352.0,
|
| 1032 |
+
"change": -2.748998480453098,
|
| 1033 |
+
"timestamp": "2025-12-07 00:17:44"
|
| 1034 |
+
}
|
| 1035 |
+
},
|
| 1036 |
+
{
|
| 1037 |
+
"timestamp": "2025-12-07T00:17:44.462497",
|
| 1038 |
+
"type": "MARKET",
|
| 1039 |
+
"symbol": "TSLA",
|
| 1040 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.70% to $293.18",
|
| 1041 |
+
"details": {
|
| 1042 |
+
"price": 293.18,
|
| 1043 |
+
"change": 2.7044069221607328,
|
| 1044 |
+
"timestamp": "2025-12-07 00:17:44"
|
| 1045 |
+
}
|
| 1046 |
+
},
|
| 1047 |
+
{
|
| 1048 |
+
"timestamp": "2025-12-07T00:17:33.807942",
|
| 1049 |
+
"type": "MARKET",
|
| 1050 |
+
"symbol": "GOOGL",
|
| 1051 |
+
"message": "\ud83d\udcc8 UP ALERT: GOOGL moved +0.86% to $147.04",
|
| 1052 |
+
"details": {
|
| 1053 |
+
"price": 147.04,
|
| 1054 |
+
"change": 0.8573976267233693,
|
| 1055 |
+
"timestamp": "2025-12-07 00:17:33"
|
| 1056 |
+
}
|
| 1057 |
+
},
|
| 1058 |
+
{
|
| 1059 |
+
"timestamp": "2025-12-07T00:17:33.570368",
|
| 1060 |
+
"type": "MARKET",
|
| 1061 |
+
"symbol": "AMZN",
|
| 1062 |
+
"message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.69% to $181.88",
|
| 1063 |
+
"details": {
|
| 1064 |
+
"price": 181.88,
|
| 1065 |
+
"change": 1.6941571149007555,
|
| 1066 |
+
"timestamp": "2025-12-07 00:17:33"
|
| 1067 |
+
}
|
| 1068 |
+
},
|
| 1069 |
+
{
|
| 1070 |
+
"timestamp": "2025-12-07T00:17:33.327027",
|
| 1071 |
+
"type": "MARKET",
|
| 1072 |
+
"symbol": "MSFT",
|
| 1073 |
+
"message": "\ud83d\udcc8 UP ALERT: MSFT moved +3.55% to $417.01",
|
| 1074 |
+
"details": {
|
| 1075 |
+
"price": 417.01,
|
| 1076 |
+
"change": 3.5457999155761857,
|
| 1077 |
+
"timestamp": "2025-12-07 00:17:33"
|
| 1078 |
+
}
|
| 1079 |
+
},
|
| 1080 |
+
{
|
| 1081 |
+
"timestamp": "2025-12-07T00:17:33.053890",
|
| 1082 |
+
"type": "MARKET",
|
| 1083 |
+
"symbol": "NVDA",
|
| 1084 |
+
"message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -2.75% to $352.00",
|
| 1085 |
+
"details": {
|
| 1086 |
+
"price": 352.0,
|
| 1087 |
+
"change": -2.748998480453098,
|
| 1088 |
+
"timestamp": "2025-12-07 00:17:33"
|
| 1089 |
+
}
|
| 1090 |
+
},
|
| 1091 |
+
{
|
| 1092 |
+
"timestamp": "2025-12-07T00:17:32.778805",
|
| 1093 |
+
"type": "MARKET",
|
| 1094 |
+
"symbol": "TSLA",
|
| 1095 |
+
"message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.70% to $293.18",
|
| 1096 |
+
"details": {
|
| 1097 |
+
"price": 293.18,
|
| 1098 |
+
"change": 2.7044069221607328,
|
| 1099 |
+
"timestamp": "2025-12-07 00:17:32"
|
| 1100 |
+
}
|
| 1101 |
+
}
|
| 1102 |
+
]
|
alphavantage_mcp.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# alphavantage_mcp.py (Corrected for Free Tier)
|
| 2 |
+
from fastapi import FastAPI, HTTPException
|
| 3 |
+
import uvicorn
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from alpha_vantage.timeseries import TimeSeries
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
# --- Configuration ---
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
# --- Logging Setup (MUST be before we use logger) ---
|
| 13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 14 |
+
logger = logging.getLogger("AlphaVantage_MCP_Server")
|
| 15 |
+
|
| 16 |
+
# --- Get API Key ---
|
| 17 |
+
ALPHA_VANTAGE_API_KEY = os.getenv("ALPHA_VANTAGE_API_KEY")
|
| 18 |
+
|
| 19 |
+
# Fallback: Try to read from Streamlit secrets file (for cloud deployment)
|
| 20 |
+
if not ALPHA_VANTAGE_API_KEY:
|
| 21 |
+
try:
|
| 22 |
+
import toml
|
| 23 |
+
secrets_path = os.path.join(os.path.dirname(__file__), ".streamlit", "secrets.toml")
|
| 24 |
+
if os.path.exists(secrets_path):
|
| 25 |
+
secrets = toml.load(secrets_path)
|
| 26 |
+
ALPHA_VANTAGE_API_KEY = secrets.get("ALPHA_VANTAGE_API_KEY")
|
| 27 |
+
logger.info("Loaded ALPHA_VANTAGE_API_KEY from .streamlit/secrets.toml")
|
| 28 |
+
except Exception as e:
|
| 29 |
+
logger.warning(f"Could not load from secrets.toml: {e}")
|
| 30 |
+
|
| 31 |
+
if not ALPHA_VANTAGE_API_KEY:
|
| 32 |
+
logger.warning("ALPHA_VANTAGE_API_KEY not found in environment. Market data features will fail.")
|
| 33 |
+
else:
|
| 34 |
+
logger.info(f"ALPHA_VANTAGE_API_KEY found: {ALPHA_VANTAGE_API_KEY[:4]}...")
|
| 35 |
+
|
| 36 |
+
# --- FastAPI App & Alpha Vantage Client ---
|
| 37 |
+
app = FastAPI(title="Aegis Alpha Vantage MCP Server")
|
| 38 |
+
ts = TimeSeries(key=ALPHA_VANTAGE_API_KEY, output_format='json')
|
| 39 |
+
|
| 40 |
+
@app.post("/market_data")
|
| 41 |
+
async def get_market_data(payload: dict):
|
| 42 |
+
"""
|
| 43 |
+
Fetches market data using the Alpha Vantage API.
|
| 44 |
+
Supports both intraday and daily data based on time_range.
|
| 45 |
+
Expects a payload like:
|
| 46 |
+
{
|
| 47 |
+
"symbol": "NVDA",
|
| 48 |
+
"time_range": "INTRADAY" | "1D" | "3D" | "1W" | "1M" | "3M" | "1Y"
|
| 49 |
+
}
|
| 50 |
+
"""
|
| 51 |
+
symbol = payload.get("symbol")
|
| 52 |
+
time_range = payload.get("time_range", "INTRADAY")
|
| 53 |
+
|
| 54 |
+
if not symbol:
|
| 55 |
+
logger.error("Validation Error: 'symbol' is required.")
|
| 56 |
+
raise HTTPException(status_code=400, detail="'symbol' is required.")
|
| 57 |
+
|
| 58 |
+
logger.info(f"Received market data request for symbol: {symbol}, time_range: {time_range}")
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
# Route to appropriate API based on time range
|
| 62 |
+
if time_range == "INTRADAY":
|
| 63 |
+
# Intraday data (last 4-6 hours, 5-min intervals)
|
| 64 |
+
data, meta_data = ts.get_intraday(symbol=symbol, interval="5min", outputsize='compact')
|
| 65 |
+
logger.info(f"Successfully retrieved intraday data for {symbol}")
|
| 66 |
+
meta_data["Source"] = "Real API (Alpha Vantage)"
|
| 67 |
+
else:
|
| 68 |
+
# Daily data for historical ranges
|
| 69 |
+
data, meta_data = ts.get_daily(symbol=symbol, outputsize='full')
|
| 70 |
+
logger.info(f"Successfully retrieved daily data for {symbol}")
|
| 71 |
+
|
| 72 |
+
# Filter data based on time range
|
| 73 |
+
data = filter_data_by_time_range(data, time_range)
|
| 74 |
+
logger.info(f"Filtered to {len(data)} data points for time_range={time_range}")
|
| 75 |
+
meta_data["Source"] = "Real API (Alpha Vantage)"
|
| 76 |
+
|
| 77 |
+
return {"status": "success", "data": data, "meta_data": meta_data}
|
| 78 |
+
|
| 79 |
+
except Exception as e:
|
| 80 |
+
# Catch ALL exceptions to ensure fallback works
|
| 81 |
+
logger.error(f"Alpha Vantage API error for symbol {symbol}: {e}")
|
| 82 |
+
logger.warning(f"Triggering MOCK DATA fallback for {symbol} due to error.")
|
| 83 |
+
|
| 84 |
+
import random
|
| 85 |
+
import math
|
| 86 |
+
from datetime import datetime, timedelta
|
| 87 |
+
|
| 88 |
+
# Seed randomness with symbol AND date to ensure it changes daily
|
| 89 |
+
# But stays consistent within the same day
|
| 90 |
+
today_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 91 |
+
seed_value = f"{symbol}_{today_str}"
|
| 92 |
+
random.seed(seed_value)
|
| 93 |
+
|
| 94 |
+
mock_data = {}
|
| 95 |
+
current_time = datetime.now()
|
| 96 |
+
|
| 97 |
+
# Generate unique base price
|
| 98 |
+
symbol_hash = sum(ord(c) for c in symbol)
|
| 99 |
+
base_price = float(symbol_hash % 500) + 50
|
| 100 |
+
|
| 101 |
+
# Force distinct start prices for common stocks
|
| 102 |
+
if "AAPL" in symbol: base_price = 150.0
|
| 103 |
+
if "TSLA" in symbol: base_price = 250.0
|
| 104 |
+
if "NVDA" in symbol: base_price = 450.0
|
| 105 |
+
if "MSFT" in symbol: base_price = 350.0
|
| 106 |
+
if "GOOG" in symbol: base_price = 130.0
|
| 107 |
+
if "AMZN" in symbol: base_price = 140.0
|
| 108 |
+
|
| 109 |
+
# Add some daily variation to base price
|
| 110 |
+
daily_noise = (hash(today_str) % 100) / 10.0 # -5 to +5 variation
|
| 111 |
+
base_price += daily_noise
|
| 112 |
+
|
| 113 |
+
trend_direction = 1 if symbol_hash % 2 == 0 else -1
|
| 114 |
+
volatility = base_price * 0.02
|
| 115 |
+
trend_strength = base_price * 0.001
|
| 116 |
+
current_price = base_price
|
| 117 |
+
|
| 118 |
+
# Determine number of data points based on time range
|
| 119 |
+
if time_range == "INTRADAY":
|
| 120 |
+
num_points = 100
|
| 121 |
+
time_delta = timedelta(minutes=5)
|
| 122 |
+
elif time_range in ["1D", "3D"]:
|
| 123 |
+
num_points = int(time_range[0]) if time_range != "1D" else 1
|
| 124 |
+
time_delta = timedelta(days=1)
|
| 125 |
+
elif time_range == "1W":
|
| 126 |
+
num_points = 7
|
| 127 |
+
time_delta = timedelta(days=1)
|
| 128 |
+
elif time_range == "1M":
|
| 129 |
+
num_points = 30
|
| 130 |
+
time_delta = timedelta(days=1)
|
| 131 |
+
elif time_range == "3M":
|
| 132 |
+
num_points = 90
|
| 133 |
+
time_delta = timedelta(days=1)
|
| 134 |
+
elif time_range == "1Y":
|
| 135 |
+
num_points = 365
|
| 136 |
+
time_delta = timedelta(days=1)
|
| 137 |
+
else:
|
| 138 |
+
num_points = 100
|
| 139 |
+
time_delta = timedelta(minutes=5)
|
| 140 |
+
|
| 141 |
+
for i in range(num_points):
|
| 142 |
+
noise = random.uniform(-volatility, volatility)
|
| 143 |
+
cycle_1 = (base_price * 0.02) * math.sin(i / 8.0)
|
| 144 |
+
cycle_2 = (base_price * 0.01) * math.sin(i / 3.0)
|
| 145 |
+
change = noise + (trend_direction * trend_strength)
|
| 146 |
+
current_price += change
|
| 147 |
+
final_price = current_price + cycle_1 + cycle_2
|
| 148 |
+
final_price = max(1.0, final_price)
|
| 149 |
+
|
| 150 |
+
t = current_time - (time_delta * (num_points - i - 1))
|
| 151 |
+
|
| 152 |
+
# Format timestamp based on data type
|
| 153 |
+
if time_range == "INTRADAY":
|
| 154 |
+
timestamp_str = t.strftime("%Y-%m-%d %H:%M:%S")
|
| 155 |
+
else:
|
| 156 |
+
timestamp_str = t.strftime("%Y-%m-%d")
|
| 157 |
+
|
| 158 |
+
mock_data[timestamp_str] = {
|
| 159 |
+
"1. open": str(round(final_price, 2)),
|
| 160 |
+
"2. high": str(round(final_price + (volatility * 0.3), 2)),
|
| 161 |
+
"3. low": str(round(final_price - (volatility * 0.3), 2)),
|
| 162 |
+
"4. close": str(round(final_price + random.uniform(-0.1, 0.1), 2)),
|
| 163 |
+
"5. volume": str(int(random.uniform(100000, 5000000)))
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
return {
|
| 167 |
+
"status": "success",
|
| 168 |
+
"data": mock_data,
|
| 169 |
+
"meta_data": {
|
| 170 |
+
"Information": f"Mock Data ({time_range}) - API Limit/Error",
|
| 171 |
+
"Source": "Simulated (Fallback)"
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def filter_data_by_time_range(data: dict, time_range: str) -> dict:
|
| 177 |
+
"""Filter daily data to the specified time range."""
|
| 178 |
+
from datetime import datetime, timedelta
|
| 179 |
+
|
| 180 |
+
# Map time ranges to days
|
| 181 |
+
range_map = {
|
| 182 |
+
"1D": 1,
|
| 183 |
+
"3D": 3,
|
| 184 |
+
"1W": 7,
|
| 185 |
+
"1M": 30,
|
| 186 |
+
"3M": 90,
|
| 187 |
+
"1Y": 365
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
days = range_map.get(time_range, 30)
|
| 191 |
+
cutoff_date = datetime.now() - timedelta(days=days)
|
| 192 |
+
|
| 193 |
+
# Filter data
|
| 194 |
+
filtered = {}
|
| 195 |
+
for timestamp_str, values in data.items():
|
| 196 |
+
try:
|
| 197 |
+
timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d")
|
| 198 |
+
if timestamp >= cutoff_date:
|
| 199 |
+
filtered[timestamp_str] = values
|
| 200 |
+
except:
|
| 201 |
+
# If parsing fails, include the data point
|
| 202 |
+
filtered[timestamp_str] = values
|
| 203 |
+
|
| 204 |
+
return filtered
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
@app.get("/")
|
| 208 |
+
def read_root():
|
| 209 |
+
return {"message": "Aegis Alpha Vantage MCP Server is operational."}
|
| 210 |
+
|
| 211 |
+
# --- Main Execution ---
|
| 212 |
+
if __name__ == "__main__":
|
| 213 |
+
uvicorn.run(app, host="127.0.0.1", port=8002)
|
app.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
import httpx
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import json
|
| 7 |
+
import time
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import base64
|
| 10 |
+
import subprocess
|
| 11 |
+
|
| 12 |
+
# --- Path Setup ---
|
| 13 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '.')))
|
| 14 |
+
|
| 15 |
+
# --- Configuration ---
|
| 16 |
+
WATCHLIST_FILE = "watchlist.json"
|
| 17 |
+
ALERTS_FILE = "alerts.json"
|
| 18 |
+
|
| 19 |
+
# --- Page Configuration ---
|
| 20 |
+
st.set_page_config(
|
| 21 |
+
page_title="Sentinel - AI Financial Intelligence",
|
| 22 |
+
page_icon="🛡️",
|
| 23 |
+
layout="wide",
|
| 24 |
+
initial_sidebar_state="expanded"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# --- Custom CSS ---
|
| 28 |
+
def load_css(file_name):
|
| 29 |
+
with open(file_name) as f:
|
| 30 |
+
st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
|
| 31 |
+
|
| 32 |
+
load_css("style.css")
|
| 33 |
+
|
| 34 |
+
# --- Auto-Start Backend Services ---
|
| 35 |
+
# --- Auto-Start Backend Services ---
|
| 36 |
+
# --- Auto-Start Backend Services ---
|
| 37 |
+
@st.cache_resource
|
| 38 |
+
def start_background_services():
|
| 39 |
+
# Managed by main.py in production
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
# Trigger startup (cached, runs once per container)
|
| 43 |
+
start_background_services()
|
| 44 |
+
|
| 45 |
+
# --- Helper Functions ---
|
| 46 |
+
@st.cache_data(ttl=60)
|
| 47 |
+
def check_server_status():
|
| 48 |
+
urls = {"Gateway": "http://127.0.0.1:8000/", "Tavily": "http://127.0.0.1:8001/", "Alpha Vantage": "http://127.0.0.1:8002/", "Private DB": "http://127.0.0.1:8003/"}
|
| 49 |
+
statuses = {}
|
| 50 |
+
with httpx.Client(timeout=2.0) as client:
|
| 51 |
+
for name, url in urls.items():
|
| 52 |
+
try:
|
| 53 |
+
response = client.get(url)
|
| 54 |
+
statuses[name] = "✅ Online" if response.status_code == 200 else "⚠️ Error"
|
| 55 |
+
except: statuses[name] = "❌ Offline"
|
| 56 |
+
return statuses
|
| 57 |
+
|
| 58 |
+
def load_watchlist():
|
| 59 |
+
if not os.path.exists(WATCHLIST_FILE): return []
|
| 60 |
+
try:
|
| 61 |
+
with open(WATCHLIST_FILE, 'r') as f:
|
| 62 |
+
return json.load(f)
|
| 63 |
+
except:
|
| 64 |
+
return []
|
| 65 |
+
|
| 66 |
+
def save_watchlist(watchlist):
|
| 67 |
+
with open(WATCHLIST_FILE, 'w') as f: json.dump(watchlist, f)
|
| 68 |
+
|
| 69 |
+
def load_alerts():
|
| 70 |
+
if not os.path.exists(ALERTS_FILE): return []
|
| 71 |
+
try:
|
| 72 |
+
with open(ALERTS_FILE, 'r') as f:
|
| 73 |
+
return json.load(f)
|
| 74 |
+
except:
|
| 75 |
+
return []
|
| 76 |
+
|
| 77 |
+
def get_base64_image(image_path):
|
| 78 |
+
try:
|
| 79 |
+
with open(image_path, "rb") as img_file:
|
| 80 |
+
return base64.b64encode(img_file.read()).decode()
|
| 81 |
+
except Exception:
|
| 82 |
+
return ""
|
| 83 |
+
|
| 84 |
+
# --- Session State ---
|
| 85 |
+
if 'page' not in st.session_state:
|
| 86 |
+
st.session_state.page = 'home'
|
| 87 |
+
if 'analysis_complete' not in st.session_state:
|
| 88 |
+
st.session_state.analysis_complete = False
|
| 89 |
+
if 'final_state' not in st.session_state:
|
| 90 |
+
st.session_state.final_state = None
|
| 91 |
+
if 'error_message' not in st.session_state:
|
| 92 |
+
st.session_state.error_message = None
|
| 93 |
+
|
| 94 |
+
# --- UI Components ---
|
| 95 |
+
|
| 96 |
+
def render_sidebar():
|
| 97 |
+
with st.sidebar:
|
| 98 |
+
# Logo Area
|
| 99 |
+
logo_base64 = get_base64_image("assets/logo.png")
|
| 100 |
+
if logo_base64:
|
| 101 |
+
st.markdown(f"""
|
| 102 |
+
<div style="text-align: center; margin-bottom: 2rem;">
|
| 103 |
+
<img src="data:image/png;base64,{logo_base64}" style="width: 80px; height: 80px; margin-bottom: 10px;">
|
| 104 |
+
<h2 style="margin:0; font-size: 1.5rem;">SENTINEL</h2>
|
| 105 |
+
<p style="color: var(--text-secondary); font-size: 0.8rem;">AI Financial Intelligence</p>
|
| 106 |
+
</div>
|
| 107 |
+
""", unsafe_allow_html=True)
|
| 108 |
+
|
| 109 |
+
# Navigation
|
| 110 |
+
if st.button("🏠 Home", use_container_width=True):
|
| 111 |
+
st.session_state.page = 'home'
|
| 112 |
+
st.rerun()
|
| 113 |
+
|
| 114 |
+
if st.button("⚡ Analysis Console", use_container_width=True):
|
| 115 |
+
st.session_state.page = 'analysis'
|
| 116 |
+
st.rerun()
|
| 117 |
+
|
| 118 |
+
st.markdown("---")
|
| 119 |
+
|
| 120 |
+
# Settings - Completely Redesigned
|
| 121 |
+
st.markdown("### 🎯 Intelligence Configuration")
|
| 122 |
+
|
| 123 |
+
# Analysis Depth
|
| 124 |
+
st.select_slider(
|
| 125 |
+
"Analysis Depth",
|
| 126 |
+
options=["Quick Scan", "Standard", "Deep Dive", "Comprehensive"],
|
| 127 |
+
value="Standard"
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# Risk Profile
|
| 131 |
+
st.selectbox(
|
| 132 |
+
"Risk Tolerance",
|
| 133 |
+
["Conservative", "Moderate", "Aggressive", "Custom"],
|
| 134 |
+
help="Adjusts recommendation thresholds"
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Time Horizon
|
| 138 |
+
st.radio(
|
| 139 |
+
"Investment Horizon",
|
| 140 |
+
["Short-term (< 1 year)", "Medium-term (1-5 years)", "Long-term (5+ years)"],
|
| 141 |
+
index=1
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# Market Sentiment Tracking
|
| 145 |
+
st.toggle("Track Market Sentiment", value=True, help="Include social media and news sentiment analysis")
|
| 146 |
+
|
| 147 |
+
st.markdown("---")
|
| 148 |
+
|
| 149 |
+
# System Status
|
| 150 |
+
with st.expander("📡 System Status", expanded=False):
|
| 151 |
+
server_statuses = check_server_status()
|
| 152 |
+
for name, status in server_statuses.items():
|
| 153 |
+
dot_class = "status-ok" if status == "✅ Online" else "status-err"
|
| 154 |
+
st.markdown(f"""
|
| 155 |
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
| 156 |
+
<span style="font-size: 0.9rem;">{name}</span>
|
| 157 |
+
<div><span class="status-dot {dot_class}"></span><span style="font-size: 0.8rem; color: var(--text-secondary);">{status.split(' ')[1]}</span></div>
|
| 158 |
+
</div>
|
| 159 |
+
""", unsafe_allow_html=True)
|
| 160 |
+
|
| 161 |
+
# Watchlist
|
| 162 |
+
with st.expander("🛡️ Watchlist", expanded=False):
|
| 163 |
+
watchlist = load_watchlist()
|
| 164 |
+
new_symbol = st.text_input("Add Symbol:", placeholder="e.g. MSFT").upper()
|
| 165 |
+
if st.button("Add"):
|
| 166 |
+
if new_symbol and new_symbol not in watchlist:
|
| 167 |
+
watchlist.append(new_symbol)
|
| 168 |
+
save_watchlist(watchlist)
|
| 169 |
+
st.rerun()
|
| 170 |
+
|
| 171 |
+
if watchlist:
|
| 172 |
+
st.markdown("---")
|
| 173 |
+
for symbol in watchlist:
|
| 174 |
+
col1, col2 = st.columns([3, 1])
|
| 175 |
+
col1.markdown(f"**{symbol}**")
|
| 176 |
+
if col2.button("❌", key=f"del_{symbol}"):
|
| 177 |
+
watchlist.remove(symbol)
|
| 178 |
+
save_watchlist(watchlist)
|
| 179 |
+
st.rerun()
|
| 180 |
+
|
| 181 |
+
def render_home():
|
| 182 |
+
# Auto-refresh logic (Every 10s)
|
| 183 |
+
if 'last_refresh_home' not in st.session_state:
|
| 184 |
+
st.session_state.last_refresh_home = time.time()
|
| 185 |
+
|
| 186 |
+
if time.time() - st.session_state.last_refresh_home > 10:
|
| 187 |
+
st.session_state.last_refresh_home = time.time()
|
| 188 |
+
st.rerun()
|
| 189 |
+
|
| 190 |
+
# Hero Section with Logo
|
| 191 |
+
logo_base64 = get_base64_image("assets/logo.png")
|
| 192 |
+
|
| 193 |
+
if logo_base64:
|
| 194 |
+
st.markdown(f"""
|
| 195 |
+
<div class="hero-container">
|
| 196 |
+
<div style="display: flex; align-items: center; justify-content: center; gap: 20px; margin-bottom: 1.5rem;">
|
| 197 |
+
<img src="data:image/png;base64,{logo_base64}" style="width: 80px; height: 80px;">
|
| 198 |
+
<h1 class="hero-title" style="margin: 0;">Sentinel AI<br>Financial Intelligence</h1>
|
| 199 |
+
</div>
|
| 200 |
+
<p class="hero-subtitle">
|
| 201 |
+
Transform raw market data into actionable business insights with the power of AI.
|
| 202 |
+
Analyze stocks, news, and portfolios automatically using intelligent agents.
|
| 203 |
+
</p>
|
| 204 |
+
</div>
|
| 205 |
+
""", unsafe_allow_html=True)
|
| 206 |
+
else:
|
| 207 |
+
# Fallback without logo
|
| 208 |
+
st.markdown("""
|
| 209 |
+
<div class="hero-container">
|
| 210 |
+
<h1 class="hero-title">Sentinel AI<br>Financial Intelligence</h1>
|
| 211 |
+
<p class="hero-subtitle">
|
| 212 |
+
Transform raw market data into actionable business insights with the power of AI.
|
| 213 |
+
Analyze stocks, news, and portfolios automatically using intelligent agents.
|
| 214 |
+
</p>
|
| 215 |
+
</div>
|
| 216 |
+
""", unsafe_allow_html=True)
|
| 217 |
+
|
| 218 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 219 |
+
with col2:
|
| 220 |
+
if st.button("🚀 Start Analysis", use_container_width=True):
|
| 221 |
+
st.session_state.page = 'analysis'
|
| 222 |
+
st.rerun()
|
| 223 |
+
|
| 224 |
+
# Feature Cards
|
| 225 |
+
st.markdown("""
|
| 226 |
+
<div class="feature-grid">
|
| 227 |
+
<div class="feature-card">
|
| 228 |
+
<div class="feature-icon">🧠</div>
|
| 229 |
+
<div class="feature-title">Intelligent Analysis</div>
|
| 230 |
+
<div class="feature-desc">
|
| 231 |
+
Our AI automatically understands market structures, identifies patterns, and generates meaningful insights without manual configuration.
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
<div class="feature-card">
|
| 235 |
+
<div class="feature-icon">📊</div>
|
| 236 |
+
<div class="feature-title">Smart Visualizations</div>
|
| 237 |
+
<div class="feature-desc">
|
| 238 |
+
Intelligently creates the most appropriate charts and graphs for your data, with interactive visualizations.
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="feature-card">
|
| 242 |
+
<div class="feature-icon">🎯</div>
|
| 243 |
+
<div class="feature-title">Actionable Recommendations</div>
|
| 244 |
+
<div class="feature-desc">
|
| 245 |
+
Get specific, measurable recommendations for improving your portfolio based on data-driven insights.
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
""", unsafe_allow_html=True)
|
| 250 |
+
|
| 251 |
+
# --- Live Wire on Home Page ---
|
| 252 |
+
st.markdown("---")
|
| 253 |
+
st.markdown("### 🚨 Live Wire Trending")
|
| 254 |
+
|
| 255 |
+
alerts_container = st.container()
|
| 256 |
+
alerts = load_alerts()
|
| 257 |
+
if not alerts:
|
| 258 |
+
alerts_container.caption("No active alerts in feed.")
|
| 259 |
+
else:
|
| 260 |
+
for alert in reversed(alerts[-10:]): # Show last 10 on home
|
| 261 |
+
alert_type = alert.get("type", "INFO")
|
| 262 |
+
css_class = "alert-market" if alert_type == "MARKET" else "alert-news" if alert_type == "NEWS" else ""
|
| 263 |
+
icon = "📉" if alert_type == "MARKET" else "📰"
|
| 264 |
+
timestamp = datetime.fromisoformat(alert.get("timestamp", datetime.now().isoformat())).strftime("%H:%M:%S")
|
| 265 |
+
|
| 266 |
+
html = f"""
|
| 267 |
+
<div class="alert-card {css_class}">
|
| 268 |
+
<div class="alert-header">
|
| 269 |
+
<span>{icon} {alert.get("symbol")}</span>
|
| 270 |
+
<span>{timestamp}</span>
|
| 271 |
+
</div>
|
| 272 |
+
<div class="alert-body">
|
| 273 |
+
{alert.get("message")}
|
| 274 |
+
</div>
|
| 275 |
+
</div>
|
| 276 |
+
"""
|
| 277 |
+
alerts_container.markdown(html, unsafe_allow_html=True)
|
| 278 |
+
|
| 279 |
+
# Footer
|
| 280 |
+
st.markdown("<br><br><br>", unsafe_allow_html=True)
|
| 281 |
+
st.markdown("""
|
| 282 |
+
<div style="text-align: center; color: var(--text-secondary); font-size: 0.9rem;">
|
| 283 |
+
Powered by <b>Google Gemini</b> • Built with <b>LangGraph</b> • Designed with <b>Streamlit</b>
|
| 284 |
+
</div>
|
| 285 |
+
""", unsafe_allow_html=True)
|
| 286 |
+
|
| 287 |
+
def render_analysis():
|
| 288 |
+
st.markdown("## ⚡ Intelligence Directive")
|
| 289 |
+
|
| 290 |
+
# Error Display
|
| 291 |
+
if st.session_state.error_message:
|
| 292 |
+
st.error(st.session_state.error_message)
|
| 293 |
+
if st.button("Dismiss Error"):
|
| 294 |
+
st.session_state.error_message = None
|
| 295 |
+
st.rerun()
|
| 296 |
+
|
| 297 |
+
col_main, col_alerts = st.columns([3, 1.2])
|
| 298 |
+
|
| 299 |
+
with col_main:
|
| 300 |
+
with st.form("research_form", clear_on_submit=False):
|
| 301 |
+
task_input = st.text_area("Enter directive:", placeholder="e.g., Analyze the recent volatility for Tesla ($TSLA) and summarize news.", height=100)
|
| 302 |
+
submitted = st.form_submit_button("EXECUTE ANALYSIS", use_container_width=True)
|
| 303 |
+
|
| 304 |
+
if submitted and task_input:
|
| 305 |
+
st.session_state.error_message = None
|
| 306 |
+
server_statuses = check_server_status()
|
| 307 |
+
all_online = all(s == "✅ Online" for s in server_statuses.values())
|
| 308 |
+
|
| 309 |
+
if not all_online:
|
| 310 |
+
st.error("SYSTEM HALTED: Core services offline. Check sidebar status.")
|
| 311 |
+
else:
|
| 312 |
+
with st.status("🚀 SENTINEL ORCHESTRATOR ENGAGED...", expanded=True) as status:
|
| 313 |
+
try:
|
| 314 |
+
from agents.orchestrator_v3 import get_orchestrator
|
| 315 |
+
# Use default provider or env var
|
| 316 |
+
orchestrator = get_orchestrator(llm_provider="gemini")
|
| 317 |
+
|
| 318 |
+
final_state_result = {}
|
| 319 |
+
for event in orchestrator.stream({"task": task_input}):
|
| 320 |
+
agent_name = list(event.keys())[0]
|
| 321 |
+
state_update = list(event.values())[0]
|
| 322 |
+
final_state_result.update(state_update)
|
| 323 |
+
|
| 324 |
+
status.write(f"🛡️ Agent Active: {agent_name}...")
|
| 325 |
+
|
| 326 |
+
status.update(label="✅ Analysis Complete!", state="complete", expanded=False)
|
| 327 |
+
st.session_state.final_state = final_state_result
|
| 328 |
+
st.session_state.analysis_complete = True
|
| 329 |
+
st.rerun()
|
| 330 |
+
except Exception as e:
|
| 331 |
+
status.update(label="❌ System Failure", state="error")
|
| 332 |
+
st.session_state.error_message = f"RUNTIME ERROR: {e}"
|
| 333 |
+
st.rerun()
|
| 334 |
+
|
| 335 |
+
if st.session_state.analysis_complete:
|
| 336 |
+
final_state = st.session_state.final_state
|
| 337 |
+
symbol = final_state.get('symbol', 'N/A') if final_state else 'N/A'
|
| 338 |
+
|
| 339 |
+
st.markdown(f"### 📝 Report: {symbol}")
|
| 340 |
+
|
| 341 |
+
# Executive Summary
|
| 342 |
+
st.info(final_state.get("final_report", "No report generated."))
|
| 343 |
+
|
| 344 |
+
# Deep-Dive Insights
|
| 345 |
+
with st.expander("🔍 Deep-Dive Insights", expanded=True):
|
| 346 |
+
insights = final_state.get("analysis_results", {}).get("insights")
|
| 347 |
+
if insights: st.markdown(insights)
|
| 348 |
+
else: st.warning("No deep-dive insights available.")
|
| 349 |
+
|
| 350 |
+
# Charts
|
| 351 |
+
with st.expander("📊 Market Telemetry"):
|
| 352 |
+
charts = final_state.get("analysis_results", {}).get("charts", [])
|
| 353 |
+
if charts:
|
| 354 |
+
for chart in charts:
|
| 355 |
+
st.plotly_chart(chart, use_container_width=True)
|
| 356 |
+
else:
|
| 357 |
+
st.caption("No telemetry data available.")
|
| 358 |
+
|
| 359 |
+
# Raw Data
|
| 360 |
+
with st.expander("💾 Raw Intelligence Logs"):
|
| 361 |
+
tab1, tab2, tab3 = st.tabs(["Web Intelligence", "Market Data", "Internal Portfolio"])
|
| 362 |
+
with tab1: st.json(final_state.get('web_research_results', '{}'))
|
| 363 |
+
with tab2: st.json(final_state.get('market_data_results', '{}'))
|
| 364 |
+
with tab3: st.json(final_state.get('portfolio_data_results', '{}'))
|
| 365 |
+
|
| 366 |
+
if st.button("🛡️ New Analysis"):
|
| 367 |
+
st.session_state.analysis_complete = False
|
| 368 |
+
st.session_state.final_state = None
|
| 369 |
+
st.rerun()
|
| 370 |
+
|
| 371 |
+
# Live Alerts Feed
|
| 372 |
+
with col_alerts:
|
| 373 |
+
st.markdown("### 🚨 Live Wire")
|
| 374 |
+
alerts_container = st.container()
|
| 375 |
+
|
| 376 |
+
# Auto-refresh logic
|
| 377 |
+
if 'last_refresh' not in st.session_state:
|
| 378 |
+
st.session_state.last_refresh = time.time()
|
| 379 |
+
|
| 380 |
+
if time.time() - st.session_state.last_refresh > 10:
|
| 381 |
+
st.session_state.last_refresh = time.time()
|
| 382 |
+
st.rerun()
|
| 383 |
+
|
| 384 |
+
alerts = load_alerts()
|
| 385 |
+
if not alerts:
|
| 386 |
+
alerts_container.caption("No active alerts in feed.")
|
| 387 |
+
else:
|
| 388 |
+
for alert in reversed(alerts[-20:]):
|
| 389 |
+
alert_type = alert.get("type", "INFO")
|
| 390 |
+
css_class = "alert-market" if alert_type == "MARKET" else "alert-news" if alert_type == "NEWS" else ""
|
| 391 |
+
icon = "📉" if alert_type == "MARKET" else "📰"
|
| 392 |
+
timestamp = datetime.fromisoformat(alert.get("timestamp", datetime.now().isoformat())).strftime("%H:%M:%S")
|
| 393 |
+
|
| 394 |
+
html = f"""
|
| 395 |
+
<div class="alert-card {css_class}">
|
| 396 |
+
<div class="alert-header">
|
| 397 |
+
<span>{icon} {alert.get("symbol")}</span>
|
| 398 |
+
<span>{timestamp}</span>
|
| 399 |
+
</div>
|
| 400 |
+
<div class="alert-body">
|
| 401 |
+
{alert.get("message")}
|
| 402 |
+
</div>
|
| 403 |
+
</div>
|
| 404 |
+
"""
|
| 405 |
+
alerts_container.markdown(html, unsafe_allow_html=True)
|
| 406 |
+
|
| 407 |
+
render_analysis()
|
app_command_center.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app_command_center.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import httpx
|
| 6 |
+
import time
|
| 7 |
+
import json
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
# --- Path Setup ---
|
| 11 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '.')))
|
| 12 |
+
from agents.orchestrator_v3 import SentinelOrchestratorV3
|
| 13 |
+
|
| 14 |
+
# --- Configuration ---
|
| 15 |
+
WATCHLIST_FILE = "watchlist.json"
|
| 16 |
+
ALERTS_FILE = "alerts.json"
|
| 17 |
+
|
| 18 |
+
# --- Page Configuration ---
|
| 19 |
+
st.set_page_config(
|
| 20 |
+
page_title="Aegis Digital Briefing",
|
| 21 |
+
page_icon="🛡️",
|
| 22 |
+
layout="wide"
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# --- Custom CSS for the Briefing Room Theme ---
|
| 26 |
+
st.markdown("""
|
| 27 |
+
<style>
|
| 28 |
+
@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@600;700&family=Open+Sans:wght@400;600&display=swap');
|
| 29 |
+
|
| 30 |
+
html, body, [class*="st-"] {
|
| 31 |
+
font-family: 'Open Sans', sans-serif;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* Main Headers */
|
| 35 |
+
h1, h2, h3 {
|
| 36 |
+
font-family: 'Source Serif 4', serif;
|
| 37 |
+
}
|
| 38 |
+
.main-header {
|
| 39 |
+
font-size: 2.8rem;
|
| 40 |
+
font-weight: 700;
|
| 41 |
+
color: #1A202C;
|
| 42 |
+
text-align: center;
|
| 43 |
+
margin-bottom: 0.5rem;
|
| 44 |
+
}
|
| 45 |
+
.subtitle {
|
| 46 |
+
text-align: center;
|
| 47 |
+
color: #718096;
|
| 48 |
+
font-size: 1.1rem;
|
| 49 |
+
margin-bottom: 2.5rem;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* Card/Widget styling */
|
| 53 |
+
.card {
|
| 54 |
+
background-color: #FFFFFF;
|
| 55 |
+
border-radius: 8px;
|
| 56 |
+
padding: 25px;
|
| 57 |
+
border: 1px solid #E2E8F0;
|
| 58 |
+
}
|
| 59 |
+
.metric-card {
|
| 60 |
+
border-radius: 8px;
|
| 61 |
+
padding: 1.5rem;
|
| 62 |
+
text-align: center;
|
| 63 |
+
border: 1px solid #E2E8F0;
|
| 64 |
+
}
|
| 65 |
+
.metric-value {
|
| 66 |
+
font-size: 2rem;
|
| 67 |
+
font-weight: 700;
|
| 68 |
+
color: #2D3748;
|
| 69 |
+
}
|
| 70 |
+
.metric-label {
|
| 71 |
+
font-size: 0.9rem;
|
| 72 |
+
color: #A0AEC0;
|
| 73 |
+
font-weight: 600;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* Sidebar "Analyst Notes" */
|
| 77 |
+
.sidebar .st-emotion-cache-16txtl3 {
|
| 78 |
+
font-size: 1.2rem;
|
| 79 |
+
font-weight: 600;
|
| 80 |
+
color: #2D3748;
|
| 81 |
+
}
|
| 82 |
+
.note-entry {
|
| 83 |
+
background-color: #F7FAFC;
|
| 84 |
+
border-left: 4px solid #4299E1;
|
| 85 |
+
padding: 1rem;
|
| 86 |
+
border-radius: 4px;
|
| 87 |
+
margin-bottom: 0.75rem;
|
| 88 |
+
}
|
| 89 |
+
.note-title { font-weight: 600; color: #2C5282; margin-bottom: 0.25rem; }
|
| 90 |
+
.note-content { font-size: 0.85rem; color: #4A5568; }
|
| 91 |
+
|
| 92 |
+
/* Alerts Styling */
|
| 93 |
+
.alert-card {
|
| 94 |
+
padding: 1rem;
|
| 95 |
+
border-radius: 6px;
|
| 96 |
+
margin-bottom: 0.8rem;
|
| 97 |
+
border-left: 5px solid #CBD5E0;
|
| 98 |
+
background-color: #F7FAFC;
|
| 99 |
+
}
|
| 100 |
+
.alert-market { border-left-color: #E53E3E; background-color: #FFF5F5; } /* Red for Market */
|
| 101 |
+
.alert-news { border-left-color: #3182CE; background-color: #EBF8FF; } /* Blue for News */
|
| 102 |
+
.alert-header { display: flex; justify-content: space-between; font-size: 0.85rem; color: #718096; margin-bottom: 0.5rem; }
|
| 103 |
+
.alert-body { font-weight: 600; color: #2D3748; }
|
| 104 |
+
|
| 105 |
+
</style>
|
| 106 |
+
""", unsafe_allow_html=True)
|
| 107 |
+
|
| 108 |
+
# --- Helper Functions & State ---
|
| 109 |
+
@st.cache_data(ttl=60)
|
| 110 |
+
def check_server_status():
|
| 111 |
+
urls = {"Gateway": "http://127.0.0.1:8000/", "Tavily": "http://127.0.0.1:8001/",
|
| 112 |
+
"Alpha Vantage": "http://127.0.0.1:8002/", "Private DB": "http://127.0.0.1:8003/"}
|
| 113 |
+
statuses = {}
|
| 114 |
+
with httpx.Client(timeout=2.0) as client:
|
| 115 |
+
for name, url in urls.items():
|
| 116 |
+
try:
|
| 117 |
+
response = client.get(url)
|
| 118 |
+
statuses[name] = "✅ Online" if response.status_code == 200 else "⚠️ Error"
|
| 119 |
+
except:
|
| 120 |
+
statuses[name] = "❌ Offline"
|
| 121 |
+
return statuses
|
| 122 |
+
|
| 123 |
+
def load_watchlist():
|
| 124 |
+
if not os.path.exists(WATCHLIST_FILE): return []
|
| 125 |
+
try:
|
| 126 |
+
with open(WATCHLIST_FILE, 'r') as f: return json.load(f)
|
| 127 |
+
except: return []
|
| 128 |
+
|
| 129 |
+
def save_watchlist(watchlist):
|
| 130 |
+
with open(WATCHLIST_FILE, 'w') as f: json.dump(watchlist, f)
|
| 131 |
+
|
| 132 |
+
def load_alerts():
|
| 133 |
+
if not os.path.exists(ALERTS_FILE): return []
|
| 134 |
+
try:
|
| 135 |
+
with open(ALERTS_FILE, 'r') as f: return json.load(f)
|
| 136 |
+
except: return []
|
| 137 |
+
|
| 138 |
+
if 'final_state' not in st.session_state:
|
| 139 |
+
st.session_state.final_state = None
|
| 140 |
+
|
| 141 |
+
# --- UI Rendering ---
|
| 142 |
+
|
| 143 |
+
# Header
|
| 144 |
+
st.markdown('<h1 class="main-header">Aegis Digital Briefing Room</h1>', unsafe_allow_html=True)
|
| 145 |
+
st.markdown('<p class="subtitle">Automated Intelligence Reports for Modern Finance</p>', unsafe_allow_html=True)
|
| 146 |
+
|
| 147 |
+
# --- SIDEBAR: Watchlist & Notes ---
|
| 148 |
+
sidebar = st.sidebar
|
| 149 |
+
sidebar.title("🛡️ Command Center")
|
| 150 |
+
|
| 151 |
+
# 1. Watchlist Manager
|
| 152 |
+
sidebar.subheader("Active Watchlist")
|
| 153 |
+
watchlist = load_watchlist()
|
| 154 |
+
new_symbol = sidebar.text_input("Add Symbol:", placeholder="e.g. MSFT").upper()
|
| 155 |
+
if sidebar.button("Add to Watchlist"):
|
| 156 |
+
if new_symbol and new_symbol not in watchlist:
|
| 157 |
+
watchlist.append(new_symbol)
|
| 158 |
+
save_watchlist(watchlist)
|
| 159 |
+
st.rerun()
|
| 160 |
+
|
| 161 |
+
symbol_to_remove = sidebar.selectbox("Remove Symbol:", ["Select..."] + watchlist)
|
| 162 |
+
if symbol_to_remove != "Select..." and sidebar.button("Remove"):
|
| 163 |
+
watchlist.remove(symbol_to_remove)
|
| 164 |
+
save_watchlist(watchlist)
|
| 165 |
+
st.rerun()
|
| 166 |
+
|
| 167 |
+
sidebar.markdown("---")
|
| 168 |
+
|
| 169 |
+
# 2. Analyst Notes
|
| 170 |
+
sidebar.title("👨💼 Analyst's Live Notes")
|
| 171 |
+
notes_placeholder = sidebar.empty()
|
| 172 |
+
notes_placeholder.info("Awaiting new directive...")
|
| 173 |
+
|
| 174 |
+
# --- MAIN CONTENT ---
|
| 175 |
+
main_col, alerts_col = st.columns([3, 1])
|
| 176 |
+
|
| 177 |
+
with main_col:
|
| 178 |
+
# Main container for Research
|
| 179 |
+
main_container = st.container(border=True)
|
| 180 |
+
|
| 181 |
+
# Input Form
|
| 182 |
+
with main_container:
|
| 183 |
+
st.subheader("🚀 Launch On-Demand Analysis")
|
| 184 |
+
with st.form("research_form"):
|
| 185 |
+
task_input = st.text_input("", placeholder="Enter your directive, e.g., 'Analyze market reaction to the latest Apple ($AAPL) product launch'", label_visibility="collapsed")
|
| 186 |
+
submitted = st.form_submit_button("Generate Briefing", use_container_width=True)
|
| 187 |
+
|
| 188 |
+
# --- Main Logic ---
|
| 189 |
+
if submitted and task_input:
|
| 190 |
+
server_statuses = check_server_status()
|
| 191 |
+
if not all(s == "✅ Online" for s in server_statuses.values()):
|
| 192 |
+
main_container.error("Analysis cannot proceed. One or more backend services are offline. Please check the status.")
|
| 193 |
+
else:
|
| 194 |
+
# main_container.empty() # Don't clear, just show results below
|
| 195 |
+
|
| 196 |
+
final_state_result = {}
|
| 197 |
+
analyst_notes = []
|
| 198 |
+
|
| 199 |
+
try:
|
| 200 |
+
with st.spinner("Your AI Analyst is compiling the briefing... This may take a moment."):
|
| 201 |
+
for event in SentinelOrchestratorV3.stream({"task": task_input}):
|
| 202 |
+
node_name = list(event.keys())[0]
|
| 203 |
+
final_state_result.update(event[node_name])
|
| 204 |
+
|
| 205 |
+
# --- Generate and Display Live Analyst Notes ---
|
| 206 |
+
note = ""
|
| 207 |
+
if node_name == "extract_symbol":
|
| 208 |
+
note = f"Identified target entity: **{event[node_name].get('symbol', 'N/A')}**"
|
| 209 |
+
elif node_name == "web_researcher":
|
| 210 |
+
note = "Sourced initial open-source intelligence from the web."
|
| 211 |
+
elif node_name == "market_data_analyst":
|
| 212 |
+
note = "Retrieved latest intraday market performance data."
|
| 213 |
+
elif node_name == "data_analyzer":
|
| 214 |
+
note = "Commenced deep-dive quantitative analysis of time-series data."
|
| 215 |
+
elif node_name == "report_synthesizer":
|
| 216 |
+
note = "Synthesizing all findings into the final executive briefing."
|
| 217 |
+
|
| 218 |
+
if note:
|
| 219 |
+
analyst_notes.append(f'<div class="note-entry"><div class="note-title">{node_name.replace("_", " ").title()}</div><div class="note-content">{note}</div></div>')
|
| 220 |
+
notes_placeholder.markdown("".join(analyst_notes), unsafe_allow_html=True)
|
| 221 |
+
time.sleep(0.5)
|
| 222 |
+
|
| 223 |
+
# --- Display the Final Briefing ---
|
| 224 |
+
st.session_state.final_state = final_state_result
|
| 225 |
+
final_state = st.session_state.final_state
|
| 226 |
+
symbol = final_state.get("symbol", "N/A")
|
| 227 |
+
|
| 228 |
+
# HEADLINE
|
| 229 |
+
st.markdown(f"## Briefing: {symbol} - {datetime.now().strftime('%B %d, %Y')}")
|
| 230 |
+
st.markdown("---")
|
| 231 |
+
|
| 232 |
+
# KEY METRICS WIDGET
|
| 233 |
+
st.subheader("Key Performance Indicators")
|
| 234 |
+
df = final_state.get("analysis_results", {}).get("dataframe")
|
| 235 |
+
if df is not None and not df.empty:
|
| 236 |
+
m_col1, m_col2, m_col3, m_col4 = st.columns(4)
|
| 237 |
+
with m_col1:
|
| 238 |
+
st.markdown(f'<div class="metric-card"><div class="metric-value">${df["close"].iloc[-1]:.2f}</div><div class="metric-label">Latest Close Price</div></div>', unsafe_allow_html=True)
|
| 239 |
+
with m_col2:
|
| 240 |
+
st.markdown(f'<div class="metric-card"><div class="metric-value">{df["volume"].sum()/1e6:.2f}M</div><div class="metric-label">Total Volume</div></div>', unsafe_allow_html=True)
|
| 241 |
+
with m_col3:
|
| 242 |
+
st.markdown(f'<div class="metric-card"><div class="metric-value">${df["high"].max():.2f}</div><div class="metric-label">Intraday High</div></div>', unsafe_allow_html=True)
|
| 243 |
+
with m_col4:
|
| 244 |
+
st.markdown(f'<div class="metric-card"><div class="metric-value">${df["low"].min():.2f}</div><div class="metric-label">Intraday Low</div></div>', unsafe_allow_html=True)
|
| 245 |
+
else:
|
| 246 |
+
st.info("Quantitative market data was not applicable for this briefing.")
|
| 247 |
+
|
| 248 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 249 |
+
|
| 250 |
+
# MAIN BRIEFING (REPORT + CHARTS)
|
| 251 |
+
brief_col1, brief_col2 = st.columns([7, 5]) # 70/50 split
|
| 252 |
+
with brief_col1:
|
| 253 |
+
st.subheader("Executive Summary & Analysis")
|
| 254 |
+
report_html = final_state.get("final_report", "No report generated.").replace("\n", "<br>")
|
| 255 |
+
st.markdown(f'<div class="card" style="height: 100%;">{report_html}</div>', unsafe_allow_html=True)
|
| 256 |
+
|
| 257 |
+
with brief_col2:
|
| 258 |
+
st.subheader("Visual Data Debrief")
|
| 259 |
+
charts = final_state.get("analysis_results", {}).get("charts", [])
|
| 260 |
+
if charts:
|
| 261 |
+
for chart in charts:
|
| 262 |
+
st.plotly_chart(chart, use_container_width=True)
|
| 263 |
+
else:
|
| 264 |
+
st.markdown('<div class="card" style="height: 100%;"><p>No visualizations were generated for this briefing.</p></div>', unsafe_allow_html=True)
|
| 265 |
+
|
| 266 |
+
# EVIDENCE LOG
|
| 267 |
+
with st.expander("Show Evidence Log & Methodology"):
|
| 268 |
+
st.markdown("#### Open Source Intelligence (Web Research)")
|
| 269 |
+
st.json(final_state.get('web_research_results', '{}'))
|
| 270 |
+
st.markdown("#### Deep-Dive Analysis Insights")
|
| 271 |
+
st.text(final_state.get("analysis_results", {}).get("insights", "No insights."))
|
| 272 |
+
|
| 273 |
+
if st.button("Start New Briefing"):
|
| 274 |
+
st.session_state.final_state = None
|
| 275 |
+
st.rerun()
|
| 276 |
+
|
| 277 |
+
except Exception as e:
|
| 278 |
+
st.error(f"An error occurred: {e}")
|
| 279 |
+
|
| 280 |
+
# --- LIVE ALERTS FEED ---
|
| 281 |
+
with alerts_col:
|
| 282 |
+
st.subheader("🚨 Live Alerts")
|
| 283 |
+
st.caption("Real-time monitoring feed")
|
| 284 |
+
|
| 285 |
+
alerts_container = st.container(height=600)
|
| 286 |
+
|
| 287 |
+
# Auto-refresh logic for alerts
|
| 288 |
+
if 'last_refresh' not in st.session_state:
|
| 289 |
+
st.session_state.last_refresh = time.time()
|
| 290 |
+
|
| 291 |
+
# Refresh every 10 seconds
|
| 292 |
+
if time.time() - st.session_state.last_refresh > 10:
|
| 293 |
+
st.session_state.last_refresh = time.time()
|
| 294 |
+
st.rerun()
|
| 295 |
+
|
| 296 |
+
alerts = load_alerts()
|
| 297 |
+
if not alerts:
|
| 298 |
+
alerts_container.info("No active alerts.")
|
| 299 |
+
else:
|
| 300 |
+
for alert in alerts:
|
| 301 |
+
alert_type = alert.get("type", "INFO")
|
| 302 |
+
css_class = "alert-market" if alert_type == "MARKET" else "alert-news" if alert_type == "NEWS" else ""
|
| 303 |
+
icon = "📉" if alert_type == "MARKET" else "📰"
|
| 304 |
+
|
| 305 |
+
timestamp = datetime.fromisoformat(alert.get("timestamp", datetime.now().isoformat())).strftime("%H:%M")
|
| 306 |
+
|
| 307 |
+
html = f"""
|
| 308 |
+
<div class="alert-card {css_class}">
|
| 309 |
+
<div class="alert-header">
|
| 310 |
+
<span>{icon} {alert.get("symbol")}</span>
|
| 311 |
+
<span>{timestamp}</span>
|
| 312 |
+
</div>
|
| 313 |
+
<div class="alert-body">
|
| 314 |
+
{alert.get("message")}
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
"""
|
| 318 |
+
alerts_container.markdown(html, unsafe_allow_html=True)
|
create_dummy_db.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# create_dummy_db.py
|
| 2 |
+
import sqlite3
|
| 3 |
+
|
| 4 |
+
# Connect to the SQLite database (this will create the file if it doesn't exist)
|
| 5 |
+
conn = sqlite3.connect('portfolio.db')
|
| 6 |
+
cursor = conn.cursor()
|
| 7 |
+
|
| 8 |
+
# --- Create the holdings table ---
|
| 9 |
+
cursor.execute('''
|
| 10 |
+
CREATE TABLE IF NOT EXISTS holdings (
|
| 11 |
+
id INTEGER PRIMARY KEY,
|
| 12 |
+
symbol TEXT NOT NULL UNIQUE,
|
| 13 |
+
shares INTEGER NOT NULL,
|
| 14 |
+
average_cost REAL NOT NULL
|
| 15 |
+
)
|
| 16 |
+
''')
|
| 17 |
+
print("Table 'holdings' created successfully.")
|
| 18 |
+
|
| 19 |
+
# --- Insert some sample data ---
|
| 20 |
+
# Using INSERT OR IGNORE to prevent errors if you run the script multiple times
|
| 21 |
+
holdings_data = [
|
| 22 |
+
('NVDA', 1500, 250.75),
|
| 23 |
+
('AAPL', 5000, 180.20),
|
| 24 |
+
('IBM', 2500, 155.45),
|
| 25 |
+
('TSLA', 1000, 220.90)
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
cursor.executemany('''
|
| 29 |
+
INSERT OR IGNORE INTO holdings (symbol, shares, average_cost) VALUES (?, ?, ?)
|
| 30 |
+
''', holdings_data)
|
| 31 |
+
print(f"{len(holdings_data)} sample holdings inserted.")
|
| 32 |
+
|
| 33 |
+
# --- Commit the changes and close the connection ---
|
| 34 |
+
conn.commit()
|
| 35 |
+
conn.close()
|
| 36 |
+
print("Database 'portfolio.db' is set up and ready.")
|
debug_gemini.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
import google.generativeai as genai
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 8 |
+
if not api_key:
|
| 9 |
+
print("GOOGLE_API_KEY not found.")
|
| 10 |
+
exit(1)
|
| 11 |
+
|
| 12 |
+
genai.configure(api_key=api_key)
|
| 13 |
+
|
| 14 |
+
print("Listing available models...")
|
| 15 |
+
try:
|
| 16 |
+
for m in genai.list_models():
|
| 17 |
+
if 'generateContent' in m.supported_generation_methods:
|
| 18 |
+
print(f"Name: {m.name}")
|
| 19 |
+
except Exception as e:
|
| 20 |
+
print(f"Error listing models: {e}")
|
deploy_local.sh
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# deploy_local.sh - "Poor Man's Deployment"
|
| 4 |
+
# Runs Aegis services in the background using nohup.
|
| 5 |
+
# Useful if Docker is not available or not working.
|
| 6 |
+
|
| 7 |
+
echo "🚀 Starting Aegis in Production Mode (Local)..."
|
| 8 |
+
|
| 9 |
+
# 1. Activate Virtual Environment
|
| 10 |
+
source venv/bin/activate
|
| 11 |
+
|
| 12 |
+
# 2. Kill existing processes on ports
|
| 13 |
+
echo "🧹 Cleaning up ports..."
|
| 14 |
+
lsof -ti:8000,8001,8002,8003,8501 | xargs kill -9 2>/dev/null
|
| 15 |
+
|
| 16 |
+
# 3. Create logs directory
|
| 17 |
+
mkdir -p logs
|
| 18 |
+
|
| 19 |
+
# 4. Start Services in Background
|
| 20 |
+
echo "Starting Gateway..."
|
| 21 |
+
nohup uvicorn mcp_gateway:app --host 0.0.0.0 --port 8000 > logs/gateway.log 2>&1 &
|
| 22 |
+
PID_GATEWAY=$!
|
| 23 |
+
echo "Gateway PID: $PID_GATEWAY"
|
| 24 |
+
|
| 25 |
+
echo "Starting Tavily Service..."
|
| 26 |
+
nohup uvicorn tavily_mcp:app --host 0.0.0.0 --port 8001 > logs/tavily.log 2>&1 &
|
| 27 |
+
|
| 28 |
+
echo "Starting Alpha Vantage Service..."
|
| 29 |
+
nohup uvicorn alphavantage_mcp:app --host 0.0.0.0 --port 8002 > logs/alphavantage.log 2>&1 &
|
| 30 |
+
|
| 31 |
+
echo "Starting Portfolio Service..."
|
| 32 |
+
nohup uvicorn private_mcp:app --host 0.0.0.0 --port 8003 > logs/portfolio.log 2>&1 &
|
| 33 |
+
|
| 34 |
+
echo "Starting Monitor..."
|
| 35 |
+
nohup python monitor.py > logs/monitor.log 2>&1 &
|
| 36 |
+
|
| 37 |
+
echo "Starting Frontend..."
|
| 38 |
+
nohup streamlit run app.py --server.port 8501 --server.address 0.0.0.0 > logs/frontend.log 2>&1 &
|
| 39 |
+
|
| 40 |
+
echo "✅ Deployment Complete!"
|
| 41 |
+
echo "---------------------------------------------------"
|
| 42 |
+
echo "🌐 Frontend: http://localhost:8501"
|
| 43 |
+
echo "📂 Logs are being written to the 'logs/' directory."
|
| 44 |
+
echo "🛑 To stop all services, run: pkill -f 'uvicorn|streamlit|monitor.py'"
|
| 45 |
+
echo "---------------------------------------------------"
|
deployment_guide.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Aegis Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to deploy the Aegis Financial Analyst Agent using Docker and Docker Compose.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
- **Docker** and **Docker Compose** installed on your machine.
|
| 7 |
+
- An `.env` file with valid API keys (see `.env.example` or your existing `.env`).
|
| 8 |
+
|
| 9 |
+
## Deployment Steps
|
| 10 |
+
|
| 11 |
+
1. **Build and Start Services**
|
| 12 |
+
Run the following command in the project root directory:
|
| 13 |
+
```bash
|
| 14 |
+
docker-compose up --build -d
|
| 15 |
+
```
|
| 16 |
+
This will:
|
| 17 |
+
- Build the Docker image for the application.
|
| 18 |
+
- Create a network `aegis-net`.
|
| 19 |
+
- Start all services (Gateway, Microservices, Monitor, Frontend) in detached mode.
|
| 20 |
+
|
| 21 |
+
2. **Verify Deployment**
|
| 22 |
+
- **Frontend**: Access the Streamlit UI at `http://localhost:8501`.
|
| 23 |
+
- **Gateway**: `http://localhost:8000`
|
| 24 |
+
- **Services**:
|
| 25 |
+
- Tavily: `http://localhost:8001`
|
| 26 |
+
- Alpha Vantage: `http://localhost:8002`
|
| 27 |
+
- Portfolio: `http://localhost:8003`
|
| 28 |
+
|
| 29 |
+
3. **View Logs**
|
| 30 |
+
To see logs for all services:
|
| 31 |
+
```bash
|
| 32 |
+
docker-compose logs -f
|
| 33 |
+
```
|
| 34 |
+
To see logs for a specific service (e.g., frontend):
|
| 35 |
+
```bash
|
| 36 |
+
docker-compose logs -f frontend
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
4. **Stop Services**
|
| 40 |
+
To stop and remove containers:
|
| 41 |
+
```bash
|
| 42 |
+
docker-compose down
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## Environment Variables
|
| 46 |
+
Ensure your `.env` file contains:
|
| 47 |
+
|
| 48 |
+
- `GOOGLE_API_KEY`
|
| 49 |
+
- `TAVILY_API_KEY`
|
| 50 |
+
- `ALPHA_VANTAGE_API_KEY`
|
| 51 |
+
|
| 52 |
+
Docker Compose automatically reads these from the `.env` file in the same directory.
|
| 53 |
+
|
| 54 |
+
## Alternative Deployment (No Docker)
|
| 55 |
+
If you cannot run Docker, use the local deployment script:
|
| 56 |
+
```bash
|
| 57 |
+
./deploy_local.sh
|
| 58 |
+
```
|
| 59 |
+
This runs all services in the background and saves logs to a `logs/` folder.
|
| 60 |
+
|
| 61 |
+
## Troubleshooting
|
| 62 |
+
- **"Cannot connect to the Docker daemon"**: This means Docker is not running. Open **Docker Desktop** on your Mac and wait for it to start (the whale icon in the menu bar should stop animating).
|
| 63 |
+
- **Port Conflicts**: Ensure ports 8000-8003 and 8501 are free.
|
| 64 |
+
- **Database Persistence**: The `portfolio.db` file is mounted as a volume, so your internal portfolio data persists across restarts.
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
gateway:
|
| 5 |
+
build: .
|
| 6 |
+
command: uvicorn mcp_gateway:app --host 0.0.0.0 --port 8000
|
| 7 |
+
ports:
|
| 8 |
+
- "8000:8000"
|
| 9 |
+
environment:
|
| 10 |
+
- TAVILY_MCP_URL=http://tavily:8001/research
|
| 11 |
+
- ALPHAVANTAGE_MCP_URL=http://alphavantage:8002/market_data
|
| 12 |
+
- PRIVATE_MCP_URL=http://portfolio:8003/portfolio_data
|
| 13 |
+
- TAVILY_API_KEY=${TAVILY_API_KEY}
|
| 14 |
+
- ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
|
| 15 |
+
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
|
| 16 |
+
|
| 17 |
+
networks:
|
| 18 |
+
- aegis-net
|
| 19 |
+
|
| 20 |
+
tavily:
|
| 21 |
+
build: .
|
| 22 |
+
command: uvicorn tavily_mcp:app --host 0.0.0.0 --port 8001
|
| 23 |
+
ports:
|
| 24 |
+
- "8001:8001"
|
| 25 |
+
environment:
|
| 26 |
+
- TAVILY_API_KEY=${TAVILY_API_KEY}
|
| 27 |
+
networks:
|
| 28 |
+
- aegis-net
|
| 29 |
+
|
| 30 |
+
alphavantage:
|
| 31 |
+
build: .
|
| 32 |
+
command: uvicorn alphavantage_mcp:app --host 0.0.0.0 --port 8002
|
| 33 |
+
ports:
|
| 34 |
+
- "8002:8002"
|
| 35 |
+
environment:
|
| 36 |
+
- ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
|
| 37 |
+
networks:
|
| 38 |
+
- aegis-net
|
| 39 |
+
|
| 40 |
+
portfolio:
|
| 41 |
+
build: .
|
| 42 |
+
command: uvicorn private_mcp:app --host 0.0.0.0 --port 8003
|
| 43 |
+
ports:
|
| 44 |
+
- "8003:8003"
|
| 45 |
+
volumes:
|
| 46 |
+
- ./portfolio.db:/app/portfolio.db
|
| 47 |
+
networks:
|
| 48 |
+
- aegis-net
|
| 49 |
+
|
| 50 |
+
monitor:
|
| 51 |
+
build: .
|
| 52 |
+
command: python monitor.py
|
| 53 |
+
environment:
|
| 54 |
+
- MCP_GATEWAY_URL=http://gateway:8000/route_agent_request
|
| 55 |
+
- TAVILY_API_KEY=${TAVILY_API_KEY}
|
| 56 |
+
- ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
|
| 57 |
+
depends_on:
|
| 58 |
+
- gateway
|
| 59 |
+
networks:
|
| 60 |
+
- aegis-net
|
| 61 |
+
|
| 62 |
+
frontend:
|
| 63 |
+
build: .
|
| 64 |
+
command: streamlit run app.py --server.port 8501 --server.address 0.0.0.0
|
| 65 |
+
ports:
|
| 66 |
+
- "8501:8501"
|
| 67 |
+
environment:
|
| 68 |
+
- MCP_GATEWAY_URL=http://gateway:8000/route_agent_request
|
| 69 |
+
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
|
| 70 |
+
|
| 71 |
+
depends_on:
|
| 72 |
+
- gateway
|
| 73 |
+
networks:
|
| 74 |
+
- aegis-net
|
| 75 |
+
|
| 76 |
+
networks:
|
| 77 |
+
aegis-net:
|
| 78 |
+
driver: bridge
|
linkedin_post.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 LinkedIn Post Drafts for Aegis / Sentinel
|
| 2 |
+
|
| 3 |
+
Here are three variations for your LinkedIn post, depending on the angle you want to take.
|
| 4 |
+
|
| 5 |
+
## Option 1: The "Technical Deep Dive" (Best for Engineering Networks)
|
| 6 |
+
**Headline:** Building a Proactive Financial AI Agent with LangGraph & Microservices 🛡️
|
| 7 |
+
|
| 8 |
+
I just wrapped up building **Aegis**, a "Premium Financial Terminal" powered by AI. It’s not just a chatbot; it’s a fully orchestrated agentic system.
|
| 9 |
+
|
| 10 |
+
**The Tech Stack:**
|
| 11 |
+
* **Brain:** LangGraph for stateful orchestration (Google Gemini).
|
| 12 |
+
* **Architecture:** Microservices pattern using the **Model Context Protocol (MCP)**.
|
| 13 |
+
* **Real-Time:** A background monitor that watches my portfolio and pushes alerts.
|
| 14 |
+
* **Frontend:** Streamlit for that Bloomberg Terminal vibe.
|
| 15 |
+
|
| 16 |
+
**How it works:**
|
| 17 |
+
The "Orchestrator" breaks down natural language directives (e.g., "Analyze TSLA and check my exposure"). It routes tasks through a FastAPI Gateway to specialized agents: a **Web Researcher** (Tavily), a **Market Analyst** (Alpha Vantage), and a **Portfolio Manager** (Local DB).
|
| 18 |
+
|
| 19 |
+
It’s been a great journey learning how to decouple AI tools from the core logic.
|
| 20 |
+
|
| 21 |
+
#AI #LangGraph #Python #Microservices #FinTech #LLM #OpenSource
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## Option 2: The "Product Showcase" (Best for General Audience)
|
| 26 |
+
**Headline:** Meet Sentinel: My Personal AI Financial Analyst ⚡
|
| 27 |
+
|
| 28 |
+
Tired of switching between news sites, stock charts, and my brokerage app, I decided to build my own solution.
|
| 29 |
+
|
| 30 |
+
Introducing **Sentinel** (Project Aegis) – an AI agent that acts as a proactive financial analyst.
|
| 31 |
+
|
| 32 |
+
**What it does:**
|
| 33 |
+
✅ **Deep Dives:** I ask "Why is Apple down?", and it reads the news, checks the charts, and writes a report.
|
| 34 |
+
✅ **24/7 Monitoring:** It watches my watchlist and alerts me to price spikes or breaking news.
|
| 35 |
+
✅ **Portfolio Context:** It knows what I own, so its advice is personalized.
|
| 36 |
+
|
| 37 |
+
The power of Agentic AI is that it doesn't just talk; it *does*. It plans, researches, and synthesizes information faster than I ever could.
|
| 38 |
+
|
| 39 |
+
#ArtificialIntelligence #FinTech #Productivity #Coding #Streamlit
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## Option 3: The "Learning Journey" (Best for Engagement)
|
| 44 |
+
**Headline:** From "Chatbot" to "Agentic System" – My latest build 🧠
|
| 45 |
+
|
| 46 |
+
I spent the last few days building **Aegis**, and it completely changed how I think about AI applications.
|
| 47 |
+
|
| 48 |
+
I started with a simple script, but realized that for complex tasks like financial analysis, a single LLM call isn't enough. You need **Agents**.
|
| 49 |
+
|
| 50 |
+
**Key Lessons Learned:**
|
| 51 |
+
1. **State Management is King:** Using LangGraph to pass context between a "Researcher" and a "Data Analyst" is a game changer.
|
| 52 |
+
2. **Decoupling Matters:** I built a "Gateway" so I can swap out my News provider without breaking the whole app.
|
| 53 |
+
3. **Latency vs. Accuracy:** Orchestrating multiple AI calls takes time, but the depth of insight is worth the wait.
|
| 54 |
+
|
| 55 |
+
Check out the architecture in the comments! 👇
|
| 56 |
+
|
| 57 |
+
What are you building with Agents right now?
|
| 58 |
+
|
| 59 |
+
#BuildInPublic #AI #Learning #SoftwareEngineering #TechTrends
|
logo_helper.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logo as base64 encoded string
|
| 2 |
+
# This avoids binary file issues with Hugging Face Spaces
|
| 3 |
+
|
| 4 |
+
def get_logo_base64():
|
| 5 |
+
"""Returns the Sentinel logo as a base64 encoded string"""
|
| 6 |
+
# If logo file exists, read it
|
| 7 |
+
try:
|
| 8 |
+
import base64
|
| 9 |
+
with open("assets/logo.png", "rb") as f:
|
| 10 |
+
return base64.b64encode(f.read()).decode()
|
| 11 |
+
except:
|
| 12 |
+
# Fallback: return empty string if file not found
|
| 13 |
+
return ""
|
main.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
import time
|
| 5 |
+
import signal
|
| 6 |
+
|
| 7 |
+
def cleanup(signum, frame):
|
| 8 |
+
print("Stopping services...")
|
| 9 |
+
# Add cleanup logic here if needed
|
| 10 |
+
sys.exit(0)
|
| 11 |
+
|
| 12 |
+
signal.signal(signal.SIGINT, cleanup)
|
| 13 |
+
signal.signal(signal.SIGTERM, cleanup)
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
print("🚀 Starting Sentinel Monolith...")
|
| 17 |
+
|
| 18 |
+
# 1. Start the MCP Gateway (which now includes all microservices)
|
| 19 |
+
# running on port 8000
|
| 20 |
+
gateway_cmd = [sys.executable, "mcp_gateway.py"]
|
| 21 |
+
gateway_process = subprocess.Popen(gateway_cmd, cwd=os.getcwd())
|
| 22 |
+
print(f"✅ Gateway started (PID: {gateway_process.pid})")
|
| 23 |
+
|
| 24 |
+
# 2. Start the Monitor (runs in background loop)
|
| 25 |
+
# Using the same interpreter
|
| 26 |
+
monitor_cmd = [sys.executable, "monitor.py"]
|
| 27 |
+
monitor_process = subprocess.Popen(monitor_cmd, cwd=os.getcwd())
|
| 28 |
+
print(f"✅ Monitor started (PID: {monitor_process.pid})")
|
| 29 |
+
|
| 30 |
+
# Give backend a moment to initialize
|
| 31 |
+
time.sleep(5)
|
| 32 |
+
|
| 33 |
+
# 3. Start Streamlit (Frontend)
|
| 34 |
+
# This commands blocks until Streamlit exits
|
| 35 |
+
print("✅ Starting Streamlit on port 7860...")
|
| 36 |
+
streamlit_cmd = [
|
| 37 |
+
"streamlit", "run", "app.py",
|
| 38 |
+
"--server.port", "7860",
|
| 39 |
+
"--server.address", "0.0.0.0",
|
| 40 |
+
"--server.headless", "true",
|
| 41 |
+
"--browser.serverAddress", "0.0.0.0",
|
| 42 |
+
"--server.enableCORS", "false",
|
| 43 |
+
"--server.enableXsrfProtection", "false"
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
# We use subprocess.run for the foreground process
|
| 47 |
+
subprocess.run(streamlit_cmd, check=False)
|
| 48 |
+
|
| 49 |
+
# Cleanup when streamlit exits
|
| 50 |
+
gateway_process.terminate()
|
| 51 |
+
monitor_process.terminate()
|
| 52 |
+
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
main()
|
mcp_gateway.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mcp_gateway.py
|
| 2 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 3 |
+
from fastapi.responses import JSONResponse
|
| 4 |
+
import uvicorn
|
| 5 |
+
import httpx
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
# --- Logging Setup ---
|
| 13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 14 |
+
logger = logging.getLogger("MCP_Gateway")
|
| 15 |
+
|
| 16 |
+
# --- Import Microservices for Consolidation ---
|
| 17 |
+
try:
|
| 18 |
+
from tavily_mcp import app as tavily_app
|
| 19 |
+
from alphavantage_mcp import app as alphavantage_app
|
| 20 |
+
from private_mcp import app as private_app
|
| 21 |
+
logger.info("Successfully imported microservices for consolidation.")
|
| 22 |
+
except ImportError as e:
|
| 23 |
+
logger.critical(f"Failed to import microservices: {e}")
|
| 24 |
+
raise
|
| 25 |
+
|
| 26 |
+
# --- Configuration (Updated for Monolithic Mode) ---
|
| 27 |
+
# Default to internal mounted paths on the same port (8000)
|
| 28 |
+
TAVILY_MCP_URL = os.getenv("TAVILY_MCP_URL", "http://127.0.0.1:8000/tavily/research")
|
| 29 |
+
ALPHAVANTAGE_MCP_URL = os.getenv("ALPHAVANTAGE_MCP_URL", "http://127.0.0.1:8000/alphavantage/market_data")
|
| 30 |
+
PRIVATE_MCP_URL = os.getenv("PRIVATE_MCP_URL", "http://127.0.0.1:8000/private/portfolio_data")
|
| 31 |
+
|
| 32 |
+
# --- FastAPI App ---
|
| 33 |
+
app = FastAPI(title="Aegis MCP Gateway (Monolith)")
|
| 34 |
+
|
| 35 |
+
# --- Mount Microservices ---
|
| 36 |
+
app.mount("/tavily", tavily_app)
|
| 37 |
+
app.mount("/alphavantage", alphavantage_app)
|
| 38 |
+
app.mount("/private", private_app)
|
| 39 |
+
|
| 40 |
+
client = httpx.AsyncClient()
|
| 41 |
+
|
| 42 |
+
@app.middleware("http")
|
| 43 |
+
async def audit_log_middleware(request: Request, call_next):
|
| 44 |
+
# Skip logging for internal sub-app calls to reduce noise if needed,
|
| 45 |
+
# but strictly speaking this middleware triggers for the parent app.
|
| 46 |
+
# Requests to mounted apps might bypass this or trigger it depending on path matching.
|
| 47 |
+
logger.info(f"Request received: {request.method} {request.url}")
|
| 48 |
+
response = await call_next(request)
|
| 49 |
+
return response
|
| 50 |
+
|
| 51 |
+
@app.post("/route_agent_request")
|
| 52 |
+
async def route_agent_request(request_data: dict):
|
| 53 |
+
target_service = request_data.get("target_service")
|
| 54 |
+
payload = request_data.get("payload", {})
|
| 55 |
+
|
| 56 |
+
logger.info(f"Routing request for target service: {target_service}")
|
| 57 |
+
|
| 58 |
+
url_map = {
|
| 59 |
+
"tavily_research": TAVILY_MCP_URL,
|
| 60 |
+
"alpha_vantage_market_data": ALPHAVANTAGE_MCP_URL,
|
| 61 |
+
"internal_portfolio_data": PRIVATE_MCP_URL,
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
target_url = url_map.get(target_service)
|
| 65 |
+
|
| 66 |
+
if not target_url:
|
| 67 |
+
logger.error(f"Invalid target service specified: {target_service}")
|
| 68 |
+
raise HTTPException(status_code=400, detail=f"Invalid target service: {target_service}")
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
# Self-referential call (Gateway -> Mounted App on same server)
|
| 72 |
+
# We must ensure we don't block. HTTPX AsyncClient handles this well.
|
| 73 |
+
response = await client.post(target_url, json=payload, timeout=180.0)
|
| 74 |
+
response.raise_for_status()
|
| 75 |
+
return JSONResponse(content=response.json(), status_code=response.status_code)
|
| 76 |
+
|
| 77 |
+
except httpx.HTTPStatusError as e:
|
| 78 |
+
logger.error(f"Error from microservice {target_service}: {e.response.text}")
|
| 79 |
+
raise HTTPException(status_code=e.response.status_code, detail=e.response.json())
|
| 80 |
+
except httpx.RequestError as e:
|
| 81 |
+
logger.error(f"Could not connect to microservice {target_service}: {e}")
|
| 82 |
+
raise HTTPException(status_code=503, detail=f"Service '{target_service}' is unavailable.")
|
| 83 |
+
except Exception as e:
|
| 84 |
+
logger.critical(f"An unexpected error occurred during routing: {e}")
|
| 85 |
+
raise HTTPException(status_code=500, detail="Internal server error in MCP Gateway.")
|
| 86 |
+
|
| 87 |
+
@app.get("/")
|
| 88 |
+
def read_root():
|
| 89 |
+
return {"message": "Aegis MCP Gateway (Monolithic) is operational."}
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
| 93 |
+
|
monitor.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# monitor.py
|
| 2 |
+
import time
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import logging
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from agents.tool_calling_agents import MarketDataAgent, WebResearchAgent
|
| 9 |
+
|
| 10 |
+
# --- Configuration ---
|
| 11 |
+
WATCHLIST_FILE = "watchlist.json"
|
| 12 |
+
ALERTS_FILE = "alerts.json"
|
| 13 |
+
CHECK_INTERVAL = 10 # 10 seconds (Real-time feel)
|
| 14 |
+
PRICE_ALERT_THRESHOLD = 0.5 # More sensitive alerts
|
| 15 |
+
|
| 16 |
+
# --- Logging Setup ---
|
| 17 |
+
logging.basicConfig(
|
| 18 |
+
level=logging.INFO,
|
| 19 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 20 |
+
handlers=[
|
| 21 |
+
logging.StreamHandler(sys.stdout)
|
| 22 |
+
]
|
| 23 |
+
)
|
| 24 |
+
logger = logging.getLogger("Aegis_Monitor")
|
| 25 |
+
|
| 26 |
+
# --- Initialize Agents ---
|
| 27 |
+
market_agent = MarketDataAgent()
|
| 28 |
+
web_agent = WebResearchAgent()
|
| 29 |
+
|
| 30 |
+
def load_watchlist():
|
| 31 |
+
if not os.path.exists(WATCHLIST_FILE):
|
| 32 |
+
return []
|
| 33 |
+
try:
|
| 34 |
+
with open(WATCHLIST_FILE, 'r') as f:
|
| 35 |
+
return json.load(f)
|
| 36 |
+
except Exception as e:
|
| 37 |
+
logger.error(f"Error loading watchlist: {e}")
|
| 38 |
+
return []
|
| 39 |
+
|
| 40 |
+
def save_alert(alert):
|
| 41 |
+
alerts = []
|
| 42 |
+
if os.path.exists(ALERTS_FILE):
|
| 43 |
+
try:
|
| 44 |
+
with open(ALERTS_FILE, 'r') as f:
|
| 45 |
+
alerts = json.load(f)
|
| 46 |
+
except:
|
| 47 |
+
pass
|
| 48 |
+
|
| 49 |
+
# Prepend new alert
|
| 50 |
+
alerts.insert(0, alert)
|
| 51 |
+
# Keep only last 100 alerts (increased from 50)
|
| 52 |
+
alerts = alerts[:100]
|
| 53 |
+
|
| 54 |
+
with open(ALERTS_FILE, 'w') as f:
|
| 55 |
+
json.dump(alerts, f, indent=2)
|
| 56 |
+
|
| 57 |
+
def check_market_data(symbol):
|
| 58 |
+
try:
|
| 59 |
+
logger.info(f"Checking market data for {symbol}...")
|
| 60 |
+
# Get compact intraday data (Corrected method call)
|
| 61 |
+
result = market_agent.get_market_data(symbol=symbol, time_range="INTRADAY")
|
| 62 |
+
|
| 63 |
+
if result.get("status") != "success":
|
| 64 |
+
logger.warning(f"Failed to get market data for {symbol}")
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
data = result.get("data", {})
|
| 68 |
+
if not data:
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
# Get latest and 15-minute-ago data points to calculate change
|
| 72 |
+
timestamps = sorted(list(data.keys()), reverse=True)
|
| 73 |
+
if len(timestamps) < 4: # Need at least 4 points (15 mins = 3 intervals)
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
latest = data[timestamps[0]]
|
| 77 |
+
baseline = data[timestamps[min(3, len(timestamps)-1)]] # 15 mins ago
|
| 78 |
+
|
| 79 |
+
close_latest = float(latest.get("4. close", 0))
|
| 80 |
+
close_baseline = float(baseline.get("4. close", 0))
|
| 81 |
+
|
| 82 |
+
if close_baseline == 0:
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
pct_change = ((close_latest - close_baseline) / close_baseline) * 100
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
"price": close_latest,
|
| 89 |
+
"change": pct_change,
|
| 90 |
+
"timestamp": timestamps[0]
|
| 91 |
+
}
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Error checking market data for {symbol}: {e}")
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
def check_news(symbol):
|
| 97 |
+
try:
|
| 98 |
+
logger.info(f"Checking news for {symbol}...")
|
| 99 |
+
query = f"breaking news {symbol} stock today"
|
| 100 |
+
result = web_agent.research(queries=[query], search_depth="basic")
|
| 101 |
+
|
| 102 |
+
if result.get("status") != "success":
|
| 103 |
+
return None
|
| 104 |
+
|
| 105 |
+
# Just return the first result title for now as a "headline"
|
| 106 |
+
data = result.get("data", [])
|
| 107 |
+
if data and data[0].get("results"):
|
| 108 |
+
first_hit = data[0]["results"][0]
|
| 109 |
+
return {
|
| 110 |
+
"title": first_hit.get("title"),
|
| 111 |
+
"url": first_hit.get("url"),
|
| 112 |
+
"content": first_hit.get("content")[:200] + "..."
|
| 113 |
+
}
|
| 114 |
+
return None
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"Error checking news for {symbol}: {e}")
|
| 117 |
+
return None
|
| 118 |
+
|
| 119 |
+
def run_monitor_loop():
|
| 120 |
+
logger.info("--- 🛡️ Aegis Proactive Monitor Started ---")
|
| 121 |
+
logger.info(f"Monitoring watchlist every {CHECK_INTERVAL} seconds ({CHECK_INTERVAL/60:.0f} minutes).")
|
| 122 |
+
logger.info(f"Price alert threshold: {PRICE_ALERT_THRESHOLD}%")
|
| 123 |
+
|
| 124 |
+
while True:
|
| 125 |
+
watchlist = load_watchlist()
|
| 126 |
+
if not watchlist:
|
| 127 |
+
logger.info("Watchlist is empty. Waiting...")
|
| 128 |
+
|
| 129 |
+
for symbol in watchlist:
|
| 130 |
+
try:
|
| 131 |
+
# 1. Market Check
|
| 132 |
+
market_info = check_market_data(symbol)
|
| 133 |
+
if market_info:
|
| 134 |
+
# Alert Logic: Price moved more than threshold
|
| 135 |
+
if abs(market_info['change']) > PRICE_ALERT_THRESHOLD:
|
| 136 |
+
direction = "📈 UP" if market_info['change'] > 0 else "📉 DOWN"
|
| 137 |
+
alert_msg = f"{direction} ALERT: {symbol} moved {market_info['change']:+.2f}% to ${market_info['price']:.2f}"
|
| 138 |
+
logger.info(alert_msg)
|
| 139 |
+
|
| 140 |
+
save_alert({
|
| 141 |
+
"timestamp": datetime.now().isoformat(),
|
| 142 |
+
"type": "MARKET",
|
| 143 |
+
"symbol": symbol,
|
| 144 |
+
"message": alert_msg,
|
| 145 |
+
"details": market_info
|
| 146 |
+
})
|
| 147 |
+
|
| 148 |
+
# 2. News Check (Simplified: Just log latest headline)
|
| 149 |
+
news_info = check_news(symbol)
|
| 150 |
+
if news_info:
|
| 151 |
+
# Check if this is "significant" news based on keywords
|
| 152 |
+
keywords = [
|
| 153 |
+
"acquisition", "merger", "earnings", "crash", "surge", "plunge",
|
| 154 |
+
"fda", "lawsuit", "sec", "filing", "8-k", "10-k", "insider",
|
| 155 |
+
"partnership", "deal", "bankruptcy", "recall", "investigation",
|
| 156 |
+
"upgrade", "downgrade", "target", "buyback", "dividend"
|
| 157 |
+
]
|
| 158 |
+
if any(k in news_info['title'].lower() for k in keywords):
|
| 159 |
+
alert_msg = f"📰 NEWS ALERT: {symbol} - {news_info['title']}"
|
| 160 |
+
logger.info(alert_msg)
|
| 161 |
+
|
| 162 |
+
save_alert({
|
| 163 |
+
"timestamp": datetime.now().isoformat(),
|
| 164 |
+
"type": "NEWS",
|
| 165 |
+
"symbol": symbol,
|
| 166 |
+
"message": alert_msg,
|
| 167 |
+
"details": news_info
|
| 168 |
+
})
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
logger.error(f"Error processing {symbol}: {e}")
|
| 172 |
+
|
| 173 |
+
logger.info(f"Cycle complete. Sleeping for {CHECK_INTERVAL}s...")
|
| 174 |
+
time.sleep(CHECK_INTERVAL)
|
| 175 |
+
|
| 176 |
+
if __name__ == "__main__":
|
| 177 |
+
# Ensure we can import agents
|
| 178 |
+
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
|
| 179 |
+
run_monitor_loop()
|
packages.txt
ADDED
|
File without changes
|
private_mcp.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# private_mcp.py
|
| 2 |
+
from fastapi import FastAPI, HTTPException
|
| 3 |
+
import uvicorn
|
| 4 |
+
import sqlite3
|
| 5 |
+
import logging
|
| 6 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 7 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 8 |
+
from langchain_ollama import ChatOllama
|
| 9 |
+
|
| 10 |
+
# --- Logging Setup ---
|
| 11 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 12 |
+
logger = logging.getLogger("Private_MCP_Server")
|
| 13 |
+
|
| 14 |
+
# --- Database Configuration ---
|
| 15 |
+
DB_FILE = "portfolio.db"
|
| 16 |
+
|
| 17 |
+
# --- LLM Configuration (Local Llama 3) ---
|
| 18 |
+
# This connects to the Ollama application running on your machine.
|
| 19 |
+
# Make sure Ollama and the llama3 model are running.
|
| 20 |
+
llm = ChatOllama(model="llama3", temperature=0)
|
| 21 |
+
|
| 22 |
+
# --- Text-to-SQL Prompt Engineering ---
|
| 23 |
+
# This prompt is carefully designed to make Llama 3 generate ONLY safe SQL queries.
|
| 24 |
+
text_to_sql_prompt = ChatPromptTemplate.from_messages([
|
| 25 |
+
("system",
|
| 26 |
+
"""You are a Text-to-SQL assistant. Convert the question to a read-only SQLite query for the 'holdings' table.
|
| 27 |
+
Schema: symbol (TEXT), shares (INTEGER), average_cost (REAL).
|
| 28 |
+
RULES:
|
| 29 |
+
1. SELECT only. No INSERT/UPDATE/DELETE.
|
| 30 |
+
2. Output ONLY the SQL query. No markdown.
|
| 31 |
+
"""),
|
| 32 |
+
("human", "Question: {question}")
|
| 33 |
+
])
|
| 34 |
+
|
| 35 |
+
# Create the LangChain chain for Text-to-SQL
|
| 36 |
+
sql_generation_chain = text_to_sql_prompt | llm | StrOutputParser()
|
| 37 |
+
|
| 38 |
+
# --- FastAPI App ---
|
| 39 |
+
app = FastAPI(title="Aegis Private MCP Server")
|
| 40 |
+
|
| 41 |
+
@app.on_event("startup")
|
| 42 |
+
async def startup_db():
|
| 43 |
+
"""Initialize the database with dummy data if it doesn't exist."""
|
| 44 |
+
try:
|
| 45 |
+
with sqlite3.connect(DB_FILE) as conn:
|
| 46 |
+
cursor = conn.cursor()
|
| 47 |
+
cursor.execute("""
|
| 48 |
+
CREATE TABLE IF NOT EXISTS holdings (
|
| 49 |
+
symbol TEXT PRIMARY KEY,
|
| 50 |
+
shares INTEGER,
|
| 51 |
+
average_cost REAL
|
| 52 |
+
)
|
| 53 |
+
""")
|
| 54 |
+
|
| 55 |
+
# Check if data exists
|
| 56 |
+
cursor.execute("SELECT count(*) FROM holdings")
|
| 57 |
+
if cursor.fetchone()[0] == 0:
|
| 58 |
+
logger.info("Populating database with diverse dummy data...")
|
| 59 |
+
# Expanded list of companies across sectors
|
| 60 |
+
dummy_data = [
|
| 61 |
+
# Tech
|
| 62 |
+
('AAPL', 5000, 180.20), ('MSFT', 3000, 350.50), ('GOOGL', 1500, 140.10), ('NVDA', 800, 450.00), ('AMD', 2000, 110.30),
|
| 63 |
+
('INTC', 4000, 35.40), ('CRM', 1200, 220.10), ('ADBE', 600, 550.20), ('ORCL', 2500, 115.50), ('CSCO', 3500, 52.10),
|
| 64 |
+
# Finance
|
| 65 |
+
('JPM', 2000, 150.40), ('BAC', 5000, 32.10), ('GS', 500, 340.50), ('V', 1000, 240.20), ('MA', 800, 380.10),
|
| 66 |
+
# Retail & Consumer
|
| 67 |
+
('WMT', 1500, 160.30), ('TGT', 1000, 130.50), ('COST', 400, 550.10), ('KO', 3000, 58.20), ('PEP', 2500, 170.40),
|
| 68 |
+
('PG', 2000, 150.10), ('NKE', 1200, 105.30), ('SBUX', 1800, 95.40),
|
| 69 |
+
# Healthcare
|
| 70 |
+
('JNJ', 2500, 160.20), ('PFE', 4000, 35.10), ('UNH', 600, 480.50), ('LLY', 400, 580.10), ('MRK', 2000, 110.20),
|
| 71 |
+
# Energy & Industrial
|
| 72 |
+
('XOM', 3000, 105.40), ('CVX', 2000, 150.20), ('GE', 1500, 110.50), ('CAT', 800, 280.10), ('BA', 500, 210.30),
|
| 73 |
+
# Auto
|
| 74 |
+
('TSLA', 1000, 220.90), ('F', 5000, 12.10), ('GM', 4000, 35.40)
|
| 75 |
+
]
|
| 76 |
+
cursor.executemany("INSERT INTO holdings (symbol, shares, average_cost) VALUES (?, ?, ?)", dummy_data)
|
| 77 |
+
conn.commit()
|
| 78 |
+
logger.info("Database populated successfully.")
|
| 79 |
+
else:
|
| 80 |
+
logger.info("Database already contains data.")
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"Failed to initialize database: {e}")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def execute_safe_query(query: str, params=None):
|
| 86 |
+
"""
|
| 87 |
+
Executes a SQL query after a basic safety check.
|
| 88 |
+
This is a critical security function.
|
| 89 |
+
"""
|
| 90 |
+
# SECURITY CHECK: Ensure the query is read-only.
|
| 91 |
+
if not query.strip().upper().startswith("SELECT"):
|
| 92 |
+
logger.error(f"SECURITY VIOLATION: Attempted to execute non-SELECT query: {query}")
|
| 93 |
+
raise HTTPException(status_code=403, detail="Forbidden: Only SELECT queries are allowed.")
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
with sqlite3.connect(DB_FILE) as conn:
|
| 97 |
+
conn.row_factory = sqlite3.Row # Makes results dict-like
|
| 98 |
+
cursor = conn.cursor()
|
| 99 |
+
if params:
|
| 100 |
+
cursor.execute(query, params)
|
| 101 |
+
else:
|
| 102 |
+
cursor.execute(query)
|
| 103 |
+
|
| 104 |
+
results = [dict(row) for row in cursor.fetchall()]
|
| 105 |
+
# Sanitize results: Replace None with 0 (common for SUM on empty set)
|
| 106 |
+
for row in results:
|
| 107 |
+
for key, value in row.items():
|
| 108 |
+
if value is None:
|
| 109 |
+
row[key] = 0
|
| 110 |
+
return results
|
| 111 |
+
except sqlite3.Error as e:
|
| 112 |
+
logger.error(f"Database error executing query '{query}': {e}")
|
| 113 |
+
raise HTTPException(status_code=500, detail=f"Database query failed: {e}")
|
| 114 |
+
|
| 115 |
+
@app.post("/portfolio_data")
|
| 116 |
+
async def get_portfolio_data(payload: dict):
|
| 117 |
+
"""
|
| 118 |
+
Takes a natural language question, converts it to SQL using Llama 3,
|
| 119 |
+
and executes it against the internal portfolio database.
|
| 120 |
+
"""
|
| 121 |
+
question = payload.get("question")
|
| 122 |
+
if not question:
|
| 123 |
+
raise HTTPException(status_code=400, detail="'question' is a required field.")
|
| 124 |
+
|
| 125 |
+
logger.info(f"Received portfolio data question: '{question}'")
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
# Step 1: Generate the SQL query using the local LLM
|
| 129 |
+
try:
|
| 130 |
+
generated_sql = await sql_generation_chain.ainvoke({"question": question})
|
| 131 |
+
logger.info(f"Llama 3 generated SQL: {generated_sql}")
|
| 132 |
+
except Exception as llm_error:
|
| 133 |
+
logger.warning(f"LLM generation failed (likely Ollama offline): {llm_error}. Using fallback logic.")
|
| 134 |
+
# Fallback Logic: Dynamic symbol extraction
|
| 135 |
+
import re
|
| 136 |
+
q_upper = question.upper()
|
| 137 |
+
# Look for common ticker patterns (1-5 uppercase letters)
|
| 138 |
+
matches = re.findall(r'\b[A-Z]{1,5}\b', q_upper)
|
| 139 |
+
|
| 140 |
+
found_symbol = None
|
| 141 |
+
ignored_words = ["WHAT", "IS", "THE", "TO", "OF", "FOR", "IN", "AND", "OR", "SHOW", "ME", "DATA", "STOCK", "PRICE", "DO", "WE", "OWN", "HAVE", "ANY", "EXPOSURE", "CURRENT"]
|
| 142 |
+
|
| 143 |
+
for match in matches:
|
| 144 |
+
if match not in ignored_words:
|
| 145 |
+
found_symbol = match
|
| 146 |
+
break
|
| 147 |
+
|
| 148 |
+
if found_symbol:
|
| 149 |
+
generated_sql = f"SELECT * FROM holdings WHERE symbol='{found_symbol}'"
|
| 150 |
+
else:
|
| 151 |
+
generated_sql = "SELECT * FROM holdings" # Default to showing all
|
| 152 |
+
logger.info(f"Fallback SQL generated: {generated_sql}")
|
| 153 |
+
|
| 154 |
+
# Step 2: Execute the query using our secure function
|
| 155 |
+
results = execute_safe_query(generated_sql)
|
| 156 |
+
logger.info(f"Successfully executed query and found {len(results)} records.")
|
| 157 |
+
|
| 158 |
+
return {"status": "success", "question": question, "generated_sql": generated_sql, "data": results}
|
| 159 |
+
|
| 160 |
+
except HTTPException as http_exc:
|
| 161 |
+
# Re-raise HTTP exceptions from our secure executor
|
| 162 |
+
raise http_exc
|
| 163 |
+
except Exception as e:
|
| 164 |
+
logger.critical(f"An unexpected error occurred in the portfolio data endpoint: {e}")
|
| 165 |
+
# Don't crash the client, return an empty success with error note
|
| 166 |
+
return {"status": "error", "message": str(e), "data": []}
|
| 167 |
+
|
| 168 |
+
@app.get("/")
|
| 169 |
+
def read_root():
|
| 170 |
+
return {"message": "Aegis Private MCP Server is operational."}
|
| 171 |
+
|
| 172 |
+
# --- Main Execution ---
|
| 173 |
+
if __name__ == "__main__":
|
| 174 |
+
# This server runs on port 8003
|
| 175 |
+
uvicorn.run(app, host="127.0.0.1", port=8003)
|
requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
langchain
|
| 3 |
+
langchain-core
|
| 4 |
+
langgraph
|
| 5 |
+
pydantic<3,>=2
|
| 6 |
+
pandas
|
| 7 |
+
plotly
|
| 8 |
+
python-dotenv
|
| 9 |
+
httpx
|
| 10 |
+
alpha_vantage
|
| 11 |
+
fastapi
|
| 12 |
+
uvicorn[standard]
|
| 13 |
+
tavily
|
| 14 |
+
langchain_ollama
|
| 15 |
+
langchain-google-genai
|
start_all.sh
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Function to kill all background processes on exit
|
| 4 |
+
cleanup() {
|
| 5 |
+
echo "Stopping all services..."
|
| 6 |
+
kill $(jobs -p) 2>/dev/null
|
| 7 |
+
exit
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
# Trap SIGINT (Ctrl+C) and call cleanup
|
| 11 |
+
trap cleanup SIGINT
|
| 12 |
+
|
| 13 |
+
# Cleanup existing processes to prevent port conflicts
|
| 14 |
+
echo "🧹 Cleaning up existing processes..."
|
| 15 |
+
lsof -ti:8000,8001,8002,8003,8501 | xargs kill -9 2>/dev/null || true
|
| 16 |
+
pkill -f "uvicorn" || true
|
| 17 |
+
pkill -f "streamlit" || true
|
| 18 |
+
sleep 2
|
| 19 |
+
|
| 20 |
+
echo "🚀 Starting Aegis System..."
|
| 21 |
+
|
| 22 |
+
# Check if venv exists and activate it
|
| 23 |
+
if [ -d "venv" ]; then
|
| 24 |
+
echo "🔌 Activating virtual environment..."
|
| 25 |
+
source venv/bin/activate
|
| 26 |
+
else
|
| 27 |
+
echo "⚠️ No virtual environment found. Running with system python..."
|
| 28 |
+
fi
|
| 29 |
+
|
| 30 |
+
# Start Microservices
|
| 31 |
+
echo "Starting MCP Gateway (Port 8000)..."
|
| 32 |
+
python mcp_gateway.py > mcp_gateway.log 2>&1 &
|
| 33 |
+
|
| 34 |
+
echo "Starting Tavily MCP (Port 8001)..."
|
| 35 |
+
python tavily_mcp.py > tavily_mcp.log 2>&1 &
|
| 36 |
+
|
| 37 |
+
echo "Starting Alpha Vantage MCP (Port 8002)..."
|
| 38 |
+
python alphavantage_mcp.py > alphavantage_mcp.log 2>&1 &
|
| 39 |
+
|
| 40 |
+
echo "Starting Private Portfolio MCP (Port 8003)..."
|
| 41 |
+
python private_mcp.py > private_mcp.log 2>&1 &
|
| 42 |
+
|
| 43 |
+
# Start Monitor
|
| 44 |
+
echo "Starting Proactive Monitor..."
|
| 45 |
+
python monitor.py > monitor.log 2>&1 &
|
| 46 |
+
|
| 47 |
+
# Wait a moment for services to spin up
|
| 48 |
+
sleep 3
|
| 49 |
+
|
| 50 |
+
# Start Streamlit App
|
| 51 |
+
echo "🛡️ Launching Sentinel Interface..."
|
| 52 |
+
streamlit run app.py > streamlit.log 2>&1
|
style.css
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
/* New Theme based on latest image - "Deep Dark" */
|
| 5 |
+
--bg-color: #000000;
|
| 6 |
+
--card-bg: #0c0c0c;
|
| 7 |
+
/* Slightly lighter than black */
|
| 8 |
+
--sidebar-bg: #000000;
|
| 9 |
+
--border-color: #1e1e1e;
|
| 10 |
+
--text-primary: #ffffff;
|
| 11 |
+
--text-secondary: #9ca3af;
|
| 12 |
+
/* Cool grey */
|
| 13 |
+
|
| 14 |
+
/* Accents - Minimalist */
|
| 15 |
+
--primary: #ffffff;
|
| 16 |
+
--primary-hover: #e5e5e5;
|
| 17 |
+
|
| 18 |
+
/* Semantic */
|
| 19 |
+
--success: #10b981;
|
| 20 |
+
--warning: #f59e0b;
|
| 21 |
+
--danger: #ef4444;
|
| 22 |
+
|
| 23 |
+
--font-main: 'Inter', sans-serif;
|
| 24 |
+
/* Switched to Inter for cleaner look */
|
| 25 |
+
--font-mono: 'JetBrains Mono', monospace;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* Global Reset */
|
| 29 |
+
.stApp {
|
| 30 |
+
background-color: var(--bg-color);
|
| 31 |
+
color: var(--text-primary);
|
| 32 |
+
font-family: var(--font-main);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
h1,
|
| 36 |
+
h2,
|
| 37 |
+
h3,
|
| 38 |
+
h4,
|
| 39 |
+
h5,
|
| 40 |
+
h6 {
|
| 41 |
+
font-family: var(--font-main);
|
| 42 |
+
font-weight: 600;
|
| 43 |
+
color: var(--text-primary);
|
| 44 |
+
letter-spacing: -0.02em;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* Sidebar */
|
| 48 |
+
section[data-testid="stSidebar"] {
|
| 49 |
+
background-color: var(--sidebar-bg);
|
| 50 |
+
border-right: 1px solid var(--border-color);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* Feature Cards - Exact Match Attempt */
|
| 54 |
+
.feature-grid {
|
| 55 |
+
display: grid;
|
| 56 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 57 |
+
gap: 24px;
|
| 58 |
+
/* Specific gap */
|
| 59 |
+
margin-top: 3rem;
|
| 60 |
+
padding: 0 1rem;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.feature-card {
|
| 64 |
+
background-color: #121212;
|
| 65 |
+
/* Material Dark Surface - Visible contrast against black */
|
| 66 |
+
border: 1px solid #333333;
|
| 67 |
+
/* More visible border */
|
| 68 |
+
border-radius: 16px;
|
| 69 |
+
/* Slightly more rounded */
|
| 70 |
+
padding: 40px;
|
| 71 |
+
/* Generous padding as seen in image */
|
| 72 |
+
display: flex;
|
| 73 |
+
flex-direction: column;
|
| 74 |
+
align-items: flex-start;
|
| 75 |
+
height: 100%;
|
| 76 |
+
transition: all 0.2s ease;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.feature-card:hover {
|
| 80 |
+
border-color: #333;
|
| 81 |
+
background-color: #0a0a0a;
|
| 82 |
+
transform: translateY(-2px);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.feature-icon {
|
| 86 |
+
font-size: 3rem;
|
| 87 |
+
/* Larger icons */
|
| 88 |
+
margin-bottom: 24px;
|
| 89 |
+
line-height: 1;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.feature-title {
|
| 93 |
+
font-size: 1.35rem;
|
| 94 |
+
/* Slightly larger title */
|
| 95 |
+
font-weight: 700;
|
| 96 |
+
margin-bottom: 16px;
|
| 97 |
+
color: #ffffff;
|
| 98 |
+
letter-spacing: -0.01em;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.feature-desc {
|
| 102 |
+
font-size: 1rem;
|
| 103 |
+
color: #888888;
|
| 104 |
+
/* Muted grey description */
|
| 105 |
+
line-height: 1.6;
|
| 106 |
+
font-weight: 400;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* Buttons */
|
| 110 |
+
.stButton button {
|
| 111 |
+
background-color: white;
|
| 112 |
+
color: black;
|
| 113 |
+
border: none;
|
| 114 |
+
font-weight: 500;
|
| 115 |
+
border-radius: 6px;
|
| 116 |
+
/* Slightly squarer */
|
| 117 |
+
padding: 0.75rem 2rem;
|
| 118 |
+
transition: all 0.2s ease;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.stButton button:hover {
|
| 122 |
+
background-color: #e5e5e5;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* Inputs */
|
| 126 |
+
.stTextInput input,
|
| 127 |
+
.stTextArea textarea,
|
| 128 |
+
.stSelectbox div[data-baseweb="select"] {
|
| 129 |
+
background-color: #111 !important;
|
| 130 |
+
color: white !important;
|
| 131 |
+
border: 1px solid #333 !important;
|
| 132 |
+
border-radius: 6px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.stTextInput input:focus,
|
| 136 |
+
.stTextArea textarea:focus {
|
| 137 |
+
border-color: #666 !important;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Hero */
|
| 141 |
+
.hero-container {
|
| 142 |
+
display: flex;
|
| 143 |
+
flex-direction: column;
|
| 144 |
+
align-items: center;
|
| 145 |
+
justify-content: center;
|
| 146 |
+
text-align: center;
|
| 147 |
+
padding: 6rem 1rem 4rem 1rem;
|
| 148 |
+
width: 100%;
|
| 149 |
+
margin: 0 auto;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.hero-title {
|
| 153 |
+
font-size: 4.5rem;
|
| 154 |
+
font-weight: 800;
|
| 155 |
+
margin-bottom: 1.5rem;
|
| 156 |
+
color: white;
|
| 157 |
+
background: none;
|
| 158 |
+
/* Removed gradient for solid white punch */
|
| 159 |
+
-webkit-text-fill-color: white;
|
| 160 |
+
text-align: center;
|
| 161 |
+
width: 100%;
|
| 162 |
+
letter-spacing: -0.03em;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.hero-subtitle {
|
| 166 |
+
font-size: 1.25rem;
|
| 167 |
+
color: var(--text-secondary);
|
| 168 |
+
max-width: 700px;
|
| 169 |
+
margin: 0 auto 2.5rem auto;
|
| 170 |
+
text-align: center;
|
| 171 |
+
line-height: 1.6;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/* Alert Cards */
|
| 175 |
+
.alert-card {
|
| 176 |
+
background: var(--card-bg);
|
| 177 |
+
border: 1px solid var(--border-color);
|
| 178 |
+
padding: 16px;
|
| 179 |
+
margin-bottom: 12px;
|
| 180 |
+
border-radius: 8px;
|
| 181 |
+
transition: border-color 0.2s ease;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.alert-card:hover {
|
| 185 |
+
border-color: #333;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/* Market alerts - Red accent */
|
| 189 |
+
.alert-market {
|
| 190 |
+
border-left: 3px solid #ef4444;
|
| 191 |
+
background: linear-gradient(90deg, rgba(239, 68, 68, 0.05) 0%, var(--card-bg) 100%);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/* News alerts - Blue accent */
|
| 195 |
+
.alert-news {
|
| 196 |
+
border-left: 3px solid #3b82f6;
|
| 197 |
+
background: linear-gradient(90deg, rgba(59, 130, 246, 0.05) 0%, var(--card-bg) 100%);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.alert-header {
|
| 201 |
+
display: flex;
|
| 202 |
+
justify-content: space-between;
|
| 203 |
+
color: var(--text-secondary);
|
| 204 |
+
font-size: 0.85rem;
|
| 205 |
+
margin-bottom: 8px;
|
| 206 |
+
font-weight: 500;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.alert-body {
|
| 210 |
+
color: var(--text-primary);
|
| 211 |
+
font-size: 0.95rem;
|
| 212 |
+
}
|
tavily_mcp.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tavily_mcp.py (Corrected Version)
|
| 2 |
+
from fastapi import FastAPI, HTTPException
|
| 3 |
+
import uvicorn
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from tavily import TavilyClient
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
# --- Configuration ---
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
# --- Logging Setup (MUST be before we use logger) ---
|
| 13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 14 |
+
logger = logging.getLogger("Tavily_MCP_Server")
|
| 15 |
+
|
| 16 |
+
# --- Get API Key ---
|
| 17 |
+
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
|
| 18 |
+
|
| 19 |
+
# Fallback: Try to read from Streamlit secrets file (for cloud deployment)
|
| 20 |
+
if not TAVILY_API_KEY:
|
| 21 |
+
try:
|
| 22 |
+
import toml
|
| 23 |
+
secrets_path = os.path.join(os.path.dirname(__file__), ".streamlit", "secrets.toml")
|
| 24 |
+
if os.path.exists(secrets_path):
|
| 25 |
+
secrets = toml.load(secrets_path)
|
| 26 |
+
TAVILY_API_KEY = secrets.get("TAVILY_API_KEY")
|
| 27 |
+
logger.info("Loaded TAVILY_API_KEY from .streamlit/secrets.toml")
|
| 28 |
+
except Exception as e:
|
| 29 |
+
logger.warning(f"Could not load from secrets.toml: {e}")
|
| 30 |
+
|
| 31 |
+
if not TAVILY_API_KEY:
|
| 32 |
+
logger.warning("TAVILY_API_KEY not found in environment. Search features will fail.")
|
| 33 |
+
else:
|
| 34 |
+
logger.info(f"TAVILY_API_KEY found: {TAVILY_API_KEY[:4]}...")
|
| 35 |
+
|
| 36 |
+
# --- FastAPI App & Tavily Client ---
|
| 37 |
+
app = FastAPI(title="Aegis Tavily MCP Server")
|
| 38 |
+
tavily = TavilyClient(api_key=TAVILY_API_KEY)
|
| 39 |
+
|
| 40 |
+
@app.post("/research")
|
| 41 |
+
async def perform_research(payload: dict):
|
| 42 |
+
"""
|
| 43 |
+
Performs a search for each query using the Tavily API.
|
| 44 |
+
Expects a payload like:
|
| 45 |
+
{
|
| 46 |
+
"queries": ["query1", "query2"],
|
| 47 |
+
"search_depth": "basic" or "advanced" (optional, default basic)
|
| 48 |
+
}
|
| 49 |
+
"""
|
| 50 |
+
queries = payload.get("queries")
|
| 51 |
+
search_depth = payload.get("search_depth", "basic")
|
| 52 |
+
|
| 53 |
+
if not queries or not isinstance(queries, list):
|
| 54 |
+
logger.error("Validation Error: 'queries' must be a non-empty list.")
|
| 55 |
+
raise HTTPException(status_code=400, detail="'queries' must be a non-empty list.")
|
| 56 |
+
|
| 57 |
+
logger.info(f"Received research request for {len(queries)} queries. Search depth: {search_depth}")
|
| 58 |
+
|
| 59 |
+
# --- THIS IS THE CORRECTED LOGIC ---
|
| 60 |
+
all_results = []
|
| 61 |
+
try:
|
| 62 |
+
# Loop through each query and perform a search
|
| 63 |
+
for query in queries:
|
| 64 |
+
logger.info(f"Performing search for query: '{query}'")
|
| 65 |
+
# The search method takes a single query string
|
| 66 |
+
response = tavily.search(
|
| 67 |
+
query=query,
|
| 68 |
+
search_depth=search_depth,
|
| 69 |
+
max_results=5
|
| 70 |
+
)
|
| 71 |
+
# Add the results for this query to our collection
|
| 72 |
+
all_results.append({"query": query, "results": response["results"]})
|
| 73 |
+
|
| 74 |
+
logger.info(f"Successfully retrieved results for all queries from Tavily API.")
|
| 75 |
+
return {"status": "success", "data": all_results}
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
logger.error(f"Tavily API Error (likely rate limit): {e}. Switching to MOCK DATA fallback.")
|
| 79 |
+
# --- FALLBACK MECHANISM ---
|
| 80 |
+
mock_results = []
|
| 81 |
+
import random
|
| 82 |
+
from datetime import datetime
|
| 83 |
+
|
| 84 |
+
# Dynamic market sentiments to rotate through
|
| 85 |
+
sentiments = ["Bullish", "Bearish", "Neutral", "Volatile", "Cautious"]
|
| 86 |
+
events = ["Earnings Surprise", "New Product Launch", "Regulatory Update", "Sector Rotation", "Macro Headwinds"]
|
| 87 |
+
|
| 88 |
+
current_time = datetime.now().strftime("%H:%M")
|
| 89 |
+
|
| 90 |
+
for query in queries:
|
| 91 |
+
# Pick random sentiment and event to diversify the "news"
|
| 92 |
+
s = random.choice(sentiments)
|
| 93 |
+
e = random.choice(events)
|
| 94 |
+
|
| 95 |
+
mock_results.append({
|
| 96 |
+
"query": query,
|
| 97 |
+
"results": [
|
| 98 |
+
{
|
| 99 |
+
"title": f"[{current_time}] Market Update: {s} Sentiment for {query}",
|
| 100 |
+
"content": f"Live market data at {current_time} indicates a {s} trend for {query}. Analysts are tracking a potential {e} that could impact short-term price action. Volume remains high as traders adjust positions.",
|
| 101 |
+
"url": "http://mock-source.com/market-update"
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"title": f"[{current_time}] Sector Alert: {e} affecting {query}",
|
| 105 |
+
"content": f"Breaking: A significant {e} is rippling through the sector, heavily influencing {query}. Experts advise monitoring key resistance levels. (Simulated Real-Time Data)",
|
| 106 |
+
"url": "http://mock-source.com/sector-alert"
|
| 107 |
+
}
|
| 108 |
+
]
|
| 109 |
+
})
|
| 110 |
+
return {"status": "success", "data": mock_results}
|
| 111 |
+
|
| 112 |
+
@app.get("/")
|
| 113 |
+
def read_root():
|
| 114 |
+
return {"message": "Aegis Tavily MCP Server is operational."}
|
| 115 |
+
|
| 116 |
+
if __name__ == "__main__":
|
| 117 |
+
uvicorn.run(app, host="127.0.0.1", port=8001)
|
test_ollama.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
def check_ollama():
|
| 4 |
+
try:
|
| 5 |
+
response = requests.get("http://localhost:11434/")
|
| 6 |
+
if response.status_code == 200:
|
| 7 |
+
print("✅ Ollama is running.")
|
| 8 |
+
else:
|
| 9 |
+
print(f"⚠️ Ollama returned status code: {response.status_code}")
|
| 10 |
+
|
| 11 |
+
# Check for models
|
| 12 |
+
response = requests.get("http://localhost:11434/api/tags")
|
| 13 |
+
if response.status_code == 200:
|
| 14 |
+
models = [m['name'] for m in response.json().get('models', [])]
|
| 15 |
+
print(f"Available models: {models}")
|
| 16 |
+
if "llama3:latest" in models or "llama3" in models:
|
| 17 |
+
print("✅ llama3 model found.")
|
| 18 |
+
else:
|
| 19 |
+
print("❌ llama3 model NOT found. Please run 'ollama pull llama3'")
|
| 20 |
+
else:
|
| 21 |
+
print("❌ Could not list models.")
|
| 22 |
+
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"❌ Error connecting to Ollama: {e}")
|
| 25 |
+
|
| 26 |
+
if __name__ == "__main__":
|
| 27 |
+
check_ollama()
|
watchlist.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
["AAPL", "TSLA", "NVDA", "MSFT", "AMZN", "GOOGL"]
|