|
from langchain_openai import ChatOpenAI |
|
from langchain_core.prompts import ChatPromptTemplate |
|
from langchain.schema import StrOutputParser |
|
from operator import itemgetter |
|
from langchain_community.embeddings import HuggingFaceEmbeddings |
|
from langchain_community.vectorstores import Chroma |
|
from .state import AgentState |
|
import os |
|
|
|
|
|
embeddings = HuggingFaceEmbeddings(model_name="Snowflake/snowflake-arctic-embed-l") |
|
|
|
|
|
vector_db = Chroma( |
|
embedding_function=embeddings, |
|
persist_directory="./chroma_db", |
|
collection_name="Security_Analysis" |
|
) |
|
|
|
|
|
TECHNICAL_RAG_PROMPT = """ |
|
CONTEXT: |
|
{context} |
|
|
|
TECHNICAL INDICATORS: |
|
{technical_data} |
|
|
|
USER PORTFOLIO INFORMATION: |
|
Risk Level: {risk_level} |
|
Investment Goals: {investment_goals} |
|
|
|
You are a financial advisor with expertise in both technical and fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd along with the technical indicators to provide insights on the stock. |
|
|
|
For each stock, analyze: |
|
1. What the technical indicators suggest about current market sentiment and potential price movements |
|
2. How the fundamental data aligns with value investing principles |
|
3. Whether the stock appears to be undervalued, fairly valued, or overvalued |
|
4. How this stock fits within the user's risk profile and investment goals |
|
|
|
Provide a balanced analysis that considers both technical signals and fundamental principles. |
|
""" |
|
|
|
technical_rag_prompt = ChatPromptTemplate.from_template(TECHNICAL_RAG_PROMPT) |
|
|
|
|
|
PORTFOLIO_RAG_PROMPT = """ |
|
CONTEXT: |
|
{context} |
|
|
|
USER PORTFOLIO INFORMATION: |
|
Risk Level: {risk_level} |
|
Investment Goals: {investment_goals} |
|
Portfolio: {portfolio} |
|
|
|
You are a financial advisor with expertise in fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd to provide insights on the user's portfolio and investment strategy. |
|
Focus on applying fundamental analysis principles to the user's specific situation. |
|
|
|
Answer the following questions: |
|
1. What would Graham and Dodd recommend for this investor with their risk level and goals? |
|
2. What fundamental analysis principles should be applied to this portfolio? |
|
3. How should this investor think about value investing in today's market? |
|
|
|
Provide your answers in a clear, concise format. |
|
""" |
|
|
|
portfolio_rag_prompt = ChatPromptTemplate.from_template(PORTFOLIO_RAG_PROMPT) |
|
|
|
|
|
BATCH_ANALYSIS_PROMPT = """ |
|
CONTEXT: |
|
{context} |
|
|
|
USER PORTFOLIO INFORMATION: |
|
Risk Level: {risk_level} |
|
Investment Goals: {investment_goals} |
|
|
|
STOCKS TO ANALYZE: |
|
{stocks_data} |
|
|
|
You are a financial advisor with expertise in both technical and fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd along with the technical indicators to provide insights on each stock. |
|
|
|
For each stock, provide a brief analysis that includes: |
|
1. What the technical indicators suggest about current market sentiment |
|
2. Whether the stock appears to be undervalued, fairly valued, or overvalued |
|
3. How this stock fits within the user's risk profile and investment goals |
|
|
|
Format your response as a JSON-like structure with ticker symbols as keys and analysis as values. |
|
""" |
|
|
|
batch_analysis_prompt = ChatPromptTemplate.from_template(BATCH_ANALYSIS_PROMPT) |
|
|
|
def setup_technical_rag_chain(): |
|
"""Set up the RAG chain for technical analysis interpretation.""" |
|
|
|
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2) |
|
|
|
|
|
retriever = vector_db.as_retriever(search_kwargs={"k": 3}) |
|
|
|
|
|
technical_rag_chain = ( |
|
{ |
|
"context": lambda x: retriever.invoke("technical analysis indicators interpretation value investing"), |
|
"technical_data": itemgetter("technical_data"), |
|
"risk_level": itemgetter("risk_level"), |
|
"investment_goals": itemgetter("investment_goals") |
|
} |
|
| technical_rag_prompt |
|
| llm |
|
| StrOutputParser() |
|
) |
|
|
|
return technical_rag_chain |
|
|
|
def setup_portfolio_rag_chain(): |
|
"""Set up the RAG chain for portfolio insights.""" |
|
|
|
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2) |
|
|
|
|
|
retriever = vector_db.as_retriever(search_kwargs={"k": 5}) |
|
|
|
|
|
portfolio_rag_chain = ( |
|
{ |
|
"context": lambda x: retriever.invoke("value investing portfolio analysis Graham Dodd"), |
|
"risk_level": itemgetter("risk_level"), |
|
"investment_goals": itemgetter("investment_goals"), |
|
"portfolio": itemgetter("portfolio") |
|
} |
|
| portfolio_rag_prompt |
|
| llm |
|
| StrOutputParser() |
|
) |
|
|
|
return portfolio_rag_chain |
|
|
|
def setup_batch_analysis_chain(): |
|
"""Set up the RAG chain for batch stock analysis.""" |
|
|
|
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2) |
|
|
|
|
|
retriever = vector_db.as_retriever(search_kwargs={"k": 4}) |
|
|
|
|
|
batch_analysis_chain = ( |
|
{ |
|
"context": lambda x: retriever.invoke("technical analysis fundamental analysis value investing"), |
|
"stocks_data": itemgetter("stocks_data"), |
|
"risk_level": itemgetter("risk_level"), |
|
"investment_goals": itemgetter("investment_goals") |
|
} |
|
| batch_analysis_prompt |
|
| llm |
|
| StrOutputParser() |
|
) |
|
|
|
return batch_analysis_chain |
|
|
|
|
|
technical_rag_chain = setup_technical_rag_chain() |
|
portfolio_rag_chain = setup_portfolio_rag_chain() |
|
batch_analysis_chain = setup_batch_analysis_chain() |
|
|
|
def get_stock_interpretation(technical_data_str, risk_level, investment_goals): |
|
"""Get RAG-based interpretation for a stock's technical data.""" |
|
return technical_rag_chain.invoke({ |
|
"technical_data": technical_data_str, |
|
"risk_level": risk_level, |
|
"investment_goals": investment_goals |
|
}) |
|
|
|
def get_portfolio_insights(portfolio_str, risk_level, investment_goals): |
|
"""Get RAG-based insights for a portfolio.""" |
|
return portfolio_rag_chain.invoke({ |
|
"risk_level": risk_level, |
|
"investment_goals": investment_goals, |
|
"portfolio": portfolio_str |
|
}) |
|
|
|
def rag_analyzer(state: AgentState) -> AgentState: |
|
"""Performs RAG-based analysis on technical data and portfolio.""" |
|
portfolio = state["portfolio_data"] |
|
risk_level = state["risk_level"] |
|
investment_goals = state["investment_goals"] |
|
technical_analysis = state.get("technical_analysis", {}) |
|
|
|
|
|
portfolio_str = "" |
|
for ticker, data in portfolio.items(): |
|
portfolio_str += f"{ticker}: {data['shares']} shares at ${data['purchase_price']:.2f}, " |
|
portfolio_str += f"current value: ${data['value']:.2f}, " |
|
portfolio_str += f"gain/loss: {data['gain_loss_pct']:.2f}%\n" |
|
|
|
|
|
portfolio_insights_text = get_portfolio_insights( |
|
portfolio_str, |
|
risk_level, |
|
investment_goals |
|
) |
|
|
|
|
|
lines = portfolio_insights_text.strip().split('\n') |
|
portfolio_insights = [] |
|
|
|
|
|
current_question = None |
|
current_answer = [] |
|
|
|
for line in lines: |
|
if line.startswith('1.') or line.startswith('2.') or line.startswith('3.'): |
|
|
|
if current_question is not None: |
|
portfolio_insights.append({ |
|
"question": current_question, |
|
"response": '\n'.join(current_answer).strip() |
|
}) |
|
current_answer = [] |
|
|
|
|
|
parts = line.split(':', 1) |
|
if len(parts) > 1: |
|
current_question = parts[0].strip() |
|
current_answer.append(parts[1].strip()) |
|
else: |
|
current_question = line.strip() |
|
elif current_question is not None: |
|
current_answer.append(line) |
|
|
|
|
|
if current_question is not None and current_answer: |
|
portfolio_insights.append({ |
|
"question": current_question, |
|
"response": '\n'.join(current_answer).strip() |
|
}) |
|
|
|
|
|
if not portfolio_insights: |
|
portfolio_insights = [ |
|
{"question": "Portfolio Analysis", "response": portfolio_insights_text} |
|
] |
|
|
|
|
|
if technical_analysis: |
|
|
|
stocks_data = "" |
|
for ticker, data in technical_analysis.items(): |
|
if "error" not in data: |
|
stocks_data += f"Stock: {ticker}\n" |
|
stocks_data += f"Current Price: ${data['current_price']:.2f}\n" |
|
stocks_data += "Technical Indicators:\n" |
|
for key, value in data['technical_indicators'].items(): |
|
stocks_data += f" {key}: {value}\n" |
|
stocks_data += "Fundamental Data:\n" |
|
for key, value in data['fundamental_data'].items(): |
|
if value is not None: |
|
stocks_data += f" {key}: {value}\n" |
|
stocks_data += "\n---\n\n" |
|
|
|
|
|
if stocks_data: |
|
try: |
|
|
|
batch_analysis_result = batch_analysis_chain.invoke({ |
|
"stocks_data": stocks_data, |
|
"risk_level": risk_level, |
|
"investment_goals": investment_goals |
|
}) |
|
|
|
|
|
try: |
|
import json |
|
import re |
|
|
|
|
|
json_match = re.search(r'\{[\s\S]*\}', batch_analysis_result) |
|
if json_match: |
|
json_str = json_match.group(0) |
|
analysis_data = json.loads(json_str) |
|
|
|
|
|
for ticker, analysis in analysis_data.items(): |
|
if ticker in technical_analysis: |
|
technical_analysis[ticker]["rag_interpretation"] = analysis |
|
else: |
|
|
|
current_ticker = None |
|
current_analysis = [] |
|
|
|
for line in batch_analysis_result.split('\n'): |
|
if ':' in line and line.split(':')[0].strip() in technical_analysis: |
|
|
|
if current_ticker is not None and current_ticker in technical_analysis: |
|
technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip() |
|
current_analysis = [] |
|
|
|
|
|
current_ticker = line.split(':')[0].strip() |
|
current_analysis.append(line.split(':', 1)[1].strip()) |
|
elif current_ticker is not None: |
|
current_analysis.append(line) |
|
|
|
|
|
if current_ticker is not None and current_ticker in technical_analysis: |
|
technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip() |
|
|
|
except (json.JSONDecodeError, Exception) as json_err: |
|
print(f"Error parsing JSON response: {str(json_err)}") |
|
|
|
print(f"Raw response: {batch_analysis_result}") |
|
|
|
|
|
current_ticker = None |
|
current_analysis = [] |
|
|
|
for line in batch_analysis_result.split('\n'): |
|
if ':' in line and line.split(':')[0].strip() in technical_analysis: |
|
|
|
if current_ticker is not None and current_ticker in technical_analysis: |
|
technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip() |
|
current_analysis = [] |
|
|
|
|
|
current_ticker = line.split(':')[0].strip() |
|
current_analysis.append(line.split(':', 1)[1].strip()) |
|
elif current_ticker is not None: |
|
current_analysis.append(line) |
|
|
|
|
|
if current_ticker is not None and current_ticker in technical_analysis: |
|
technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip() |
|
|
|
except Exception as e: |
|
|
|
import traceback |
|
print(f"Error in batch analysis: {str(e)}") |
|
print(traceback.format_exc()) |
|
for ticker, data in technical_analysis.items(): |
|
if "error" not in data: |
|
technical_analysis[ticker]["rag_interpretation"] = f"Analysis unavailable due to processing error: {str(e)}" |
|
|
|
|
|
combined_insights = "\n\n".join([f"Q: {insight['question']}\nA: {insight['response']}" for insight in portfolio_insights]) |
|
|
|
|
|
state["technical_analysis"] = technical_analysis |
|
state["rag_context"] = combined_insights |
|
state["fundamental_analysis"] = state.get("fundamental_analysis", {}) |
|
state["fundamental_analysis"]["security_analysis_insights"] = portfolio_insights |
|
|
|
|
|
state["messages"] = state.get("messages", []) + [{ |
|
"role": "ai", |
|
"content": f"[RAGAnalyzer] I've provided insights on your portfolio and individual stocks based on value investing principles from Security Analysis by Graham and Dodd." |
|
}] |
|
|
|
return state |