Phase 1: initial codes

This commit is contained in:
2026-05-26 11:04:27 -04:00
parent 03bff49523
commit f49a283059
32 changed files with 4194 additions and 0 deletions
View File
+50
View File
@@ -0,0 +1,50 @@
from __future__ import annotations
import anthropic
INPUT_COST_PER_1M = 3.0 # claude-sonnet-4-6 $/1M tokens
OUTPUT_COST_PER_1M = 15.0
class ClaudeClient:
MODEL = "claude-sonnet-4-6"
def __init__(self, api_key: str):
self._client = anthropic.Anthropic(api_key=api_key)
def ask(
self,
prompt: str,
system: str = "",
max_tokens: int = 1024,
) -> tuple[str, dict]:
messages = [{"role": "user", "content": prompt}]
kwargs = {"model": self.MODEL, "max_tokens": max_tokens, "messages": messages}
if system:
kwargs["system"] = system
response = self._client.messages.create(**kwargs)
text = response.content[0].text if response.content else ""
usage = {
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"cost_usd": self._estimate_cost(response.usage.input_tokens, response.usage.output_tokens),
}
return text, usage
def stream_ask(self, prompt: str, system: str = "", max_tokens: int = 1024):
messages = [{"role": "user", "content": prompt}]
kwargs = {"model": self.MODEL, "max_tokens": max_tokens, "messages": messages}
if system:
kwargs["system"] = system
with self._client.messages.stream(**kwargs) as stream:
for text in stream.text_stream:
yield text
def _estimate_cost(self, input_tokens: int, output_tokens: int) -> float:
return (input_tokens / 1_000_000 * INPUT_COST_PER_1M) + (output_tokens / 1_000_000 * OUTPUT_COST_PER_1M)
@staticmethod
def estimate_tokens(text: str) -> int:
return max(1, len(text) // 4)
+94
View File
@@ -0,0 +1,94 @@
STOCK_SUMMARY_SYSTEM = "You are a professional equity analyst. Be concise, factual, and avoid hype."
STOCK_SUMMARY_PROMPT = """
Ticker: {symbol}
Company: {name}
Sector: {sector}
Industry: {industry}
Market Cap: {market_cap}
P/E Ratio: {pe_ratio}
EPS: {eps}
52W High: {week_52_high} | 52W Low: {week_52_low}
Dividend Yield: {dividend_yield}
Recent News Headlines:
{headlines}
Write a concise 3-paragraph stock summary:
1. Business overview and recent performance
2. Key financial metrics analysis
3. Near-term catalysts and risks
"""
TECHNICAL_READ_SYSTEM = "You are a technical analyst specializing in chart pattern recognition."
TECHNICAL_READ_PROMPT = """
Ticker: {symbol}
Period Analyzed: {period}
Current Price: {price}
SMA20: {sma20} | SMA50: {sma50}
RSI(14): {rsi}
MACD: {macd} | Signal: {macd_signal}
Bollinger Bands: Upper {bb_upper} | Lower {bb_lower}
Recent price action: {price_action}
Provide a technical analysis in 2-3 paragraphs covering:
1. Current trend, key support/resistance levels
2. Indicator readings and what they signal
3. Actionable technical outlook (bullish/bearish/neutral)
"""
SENTIMENT_SCORE_SYSTEM = "You are a financial sentiment analyst. Classify sentiment precisely."
SENTIMENT_SCORE_PROMPT = """
Ticker: {symbol}
Analyze the sentiment of these news headlines:
{headlines}
Respond in this exact format:
OVERALL: [BULLISH/BEARISH/NEUTRAL]
SCORE: [0-100 where 0=extreme bearish, 50=neutral, 100=extreme bullish]
REASONING: [2-3 sentences explaining the dominant themes]
HEADLINE_BREAKDOWN:
[For each headline: + or - or ~ and one line explanation]
"""
PRICE_OUTLOOK_SYSTEM = "You are a quantitative strategist. Base your outlook on data, not speculation."
PRICE_OUTLOOK_PROMPT = """
Ticker: {symbol}
Current Price: {price}
Technical snapshot: RSI={rsi}, trend={trend}, momentum={momentum}
Recent news sentiment: {sentiment}
Sector performance: {sector_perf}
Provide a short-term price outlook in this format:
1-DAY BIAS: [BULLISH/BEARISH/NEUTRAL] — [confidence %] — [one-line rationale]
1-WEEK BIAS: [BULLISH/BEARISH/NEUTRAL] — [confidence %] — [one-line rationale]
1-MONTH BIAS: [BULLISH/BEARISH/NEUTRAL] — [confidence %] — [one-line rationale]
KEY RISKS: [2 bullet points]
"""
PORTFOLIO_REVIEW_SYSTEM = "You are a portfolio risk manager. Identify risks clearly and suggest actionable improvements."
PORTFOLIO_REVIEW_PROMPT = """
Portfolio Holdings:
{holdings}
Total Value: {total_value}
Cash Position: {cash}
Largest Position: {top_position}
Sector Allocation: {sector_allocation}
Provide a portfolio review covering:
1. Concentration risk (any position > 20% or sector > 40%)
2. Correlation risk (holdings that move together)
3. Missing diversification (sectors, asset classes)
4. Top 3 actionable recommendations
"""
CHAT_SYSTEM = """You are StockMind AI, an expert financial assistant integrated into a stock market application.
You have access to real-time market context provided by the user. Answer questions about stocks, crypto,
market trends, investment strategies, and financial analysis. Be concise and professional.
Always note that your analysis is for informational purposes only and not financial advice."""
+93
View File
@@ -0,0 +1,93 @@
from __future__ import annotations
from datetime import datetime, timedelta
from newsapi import NewsApiClient
from ai.claude_client import ClaudeClient
from ai.prompts import SENTIMENT_SCORE_SYSTEM, SENTIMENT_SCORE_PROMPT
def fetch_news(symbol: str, api_key: str, page_size: int = 10) -> list[dict]:
try:
client = NewsApiClient(api_key=api_key)
from_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
response = client.get_everything(
q=symbol,
language="en",
sort_by="publishedAt",
page_size=page_size,
from_param=from_date,
)
articles = response.get("articles", [])
return [
{
"title": a.get("title", ""),
"description": a.get("description", ""),
"url": a.get("url", ""),
"source": a.get("source", {}).get("name", ""),
"published_at": a.get("publishedAt", ""),
}
for a in articles
if a.get("title")
]
except Exception as e:
return []
def analyze_sentiment(symbol: str, articles: list[dict], claude: ClaudeClient) -> tuple[str, dict]:
if not articles:
return "No news available for sentiment analysis.", {}
headlines = "\n".join(
f"{i + 1}. {a['title']}" for i, a in enumerate(articles[:15])
)
prompt = SENTIMENT_SCORE_PROMPT.format(symbol=symbol, headlines=headlines)
text, usage = claude.ask(prompt, system=SENTIMENT_SCORE_SYSTEM, max_tokens=800)
return text, usage
def cache_news(symbol: str, articles: list[dict], sentiment: str = "") -> None:
from db.database import get_session
from db.models import NewsCache
from datetime import datetime
with get_session() as session:
session.query(NewsCache).filter_by(symbol=symbol).delete()
for a in articles:
session.add(NewsCache(
symbol=symbol,
title=a.get("title", ""),
description=a.get("description", ""),
url=a.get("url", ""),
source=a.get("source", ""),
published_at=a.get("published_at", ""),
sentiment=sentiment,
fetched_at=datetime.utcnow(),
))
session.commit()
def get_cached_news(symbol: str, max_age_hours: int = 1) -> list[dict] | None:
from db.database import get_session
from db.models import NewsCache
from datetime import datetime, timedelta
cutoff = datetime.utcnow() - timedelta(hours=max_age_hours)
with get_session() as session:
rows = (
session.query(NewsCache)
.filter(NewsCache.symbol == symbol, NewsCache.fetched_at > cutoff)
.order_by(NewsCache.published_at.desc())
.all()
)
if not rows:
return None
return [
{
"title": r.title,
"description": r.description,
"url": r.url,
"source": r.source,
"published_at": r.published_at,
"sentiment": r.sentiment,
}
for r in rows
]