Phase 1: initial codes
This commit is contained in:
@@ -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)
|
||||
@@ -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."""
|
||||
@@ -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
|
||||
]
|
||||
Reference in New Issue
Block a user