Spaces:
Running
Running
File size: 12,880 Bytes
ac886d3 689b436 ac886d3 689b436 3fee7c6 689b436 fecb358 1f5a1dc 7935906 689b436 a267ed2 689b436 1f5a1dc 067b5d0 0975e8a 689b436 1f5a1dc 7935906 689b436 0975e8a 689b436 7935906 689b436 7935906 689b436 7935906 689b436 7935906 689b436 fecb358 7935906 fecb358 4817f08 fecb358 689b436 a267ed2 689b436 a267ed2 689b436 fecb358 689b436 7935906 689b436 fecb358 7935906 fecb358 689b436 7935906 689b436 fecb358 689b436 fecb358 689b436 ac886d3 689b436 ac886d3 3fee7c6 ac886d3 3fee7c6 ac886d3 689b436 ac886d3 689b436 3fee7c6 689b436 ac886d3 689b436 ac886d3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
from typing import Union, Dict, TypedDict, Annotated, List
import dotenv
from IPython.display import Image, display
import json
from langgraph.graph import StateGraph, START # , END
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, AIMessage, HumanMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.graph.message import add_messages
import logging
from pydantic import BaseModel, Field
from src.fetch_data import FetchData
from src.fetch_forecast import FetchForecast
from src.technical_analysis import TechnicalAnalysis
from src.fundamental_analysis import FundamentalAnalysis
from src.ticker_finder import TickerFinder
@tool
def get_stock_prices(
ticker: str
) -> Union[Dict, str]:
"""
Fetches historical stock price data and technical indicator for a given ticker.
Args:
ticker: str
The stock ticker symbol to fetch data for.
"""
df_hist = FetchData(ticker, fetchperiodinweeks=12).run()
df_past, df_fcst = FetchForecast(ticker, df_hist).run()
df, _ = TechnicalAnalysis(
ticker=ticker,
df_hist=df_hist,
df_past=df_past,
df_fcst=df_fcst,
plot_ta=False,
savefig=False,
debug=False).run()
if df_past is None:
fcst_prices = "Price forecasts could not be obtained"
fcst_returns = "Return forecasts could not be obtained"
else:
df_fcst['Date'] = df_fcst['Date'].astype(str)
fcst_prices = df_fcst[['Date','Close']].to_dict(orient='records')
fcst_returns = df_fcst[['Date','Returns']].to_dict(orient='records')
if df.shape[0] == 0:
hist_prices = "Recent price data could not be obtained"
indicators = "Indicator data could not be obtained"
else:
df['Date'] = df.index.astype(str)
# split the data into price and indicators, and take the last 10 days of data
hist_prices = df[['Date','Close', 'High', 'Low', 'Open', 'Volume']].iloc[-10:,:].to_dict(orient='records')
indicators = df[['VWAP', 'RSI', 'StochOsc', 'MACD', 'MACDsig', 'MACDdif']].iloc[-10:,:].to_dict(orient='records')
if (df_past is None) or (df.shape[0] == 0):
return f"Error fetching technical data for ticker: {ticker}"
else:
return {'recent prices': hist_prices, "forecasted prices": fcst_prices, "forecasted returns": fcst_returns, 'indicators': indicators}
@tool
def get_financial_metrics(
ticker: str
) -> Union[Dict, str]:
"""
Fetches key financial metrics for a given ticker.
Args:
ticker: str
The stock ticker symbol to fetch data for.
"""
dict_fundamentals = FundamentalAnalysis(
ticker=ticker).run()
if len(dict_fundamentals) > 0:
return dict_fundamentals
else:
return f"Error fetching financial metrics for ticker: {ticker}"
class StockAnalysisResponse(BaseModel):
"""Stock Analysis Response Schema"""
stock: str = Field(description="Stock symbol")
price_analysis: str = Field(description="Detailed analysis of stock price trends")
forecast_analysis: str = Field(description="Detailed analysis of stock price forecasts")
technical_analysis: str = Field(description="Detailed analysis of technical indicators")
fundamental_analysis: str = Field(description="Detailed analysis of financial metrics")
final_summary: str = Field(description="Conclusive summary of the analyses above")
recommended_action: str = Field(description="Suggested action based on the above analyses among options: [strong sell, sell, hold, buy, strong buy]")
class StockAnalyst():
def __init__(
self,
debug: bool=False) -> None:
"""
Initialize StockAnalyst object.
Sets up the logger, loads the .env data and builds the agent graph.
Args:
debug : bool, optional, default: False
if True, logger will be set to DEBUG level
"""
# set up logging
if debug:
self.logger_level = logging.DEBUG
else:
self.logger_level = logging.INFO
self.logger = logging.getLogger(__name__)
logging.basicConfig(level=self.logger_level) # filename='TechnicalAnalysis.log',
# load the env variables fom .env file
dotenv.load_dotenv(dotenv.find_dotenv())
# initialize the tickerfinder
self.tickerfinder = TickerFinder()
# build the graph
self.graph = self.build_graph()
self.logger.info('Initialized StockAnalyst object with TickerFinder and built the agent graph.')
def get_prompt(
self,
company: str) -> str:
"""
Generates a stock analysis prompt for a given company.
Args:
company : str
The stock symbol (ticker) of the company to analyze.
Returns: str
A formatted string prompt for stock analysis, which includes
instructions for evaluating the company's performance.
"""
stock_analyst_prompt= """
You are a stock analyst specializing in evaluating the performance of a given company (whose symbol is {company})
based on recent price data and technical indicators as well as financial metrics.
Your task is to provide a comprehensive summaries of price movements, technical and fundamental analysis for a given stock,
and based on the analysis, provide receommended action (see below) for details.
You have access to the following tools:
1. **get_stock_prices**: Retrieves the historical price data, technical indicators like VWAP, RSI, Stochastic Oscillator and MACD metrics, forecasted prices and relative returns for the next 5 business days.
2. **get_financial_metrics**: Retrieves key financial metrics, such as revenue, earnings per share (EPS), price-to-earnings ratio (P/E), and debt-to-equity ratio.
### Your Tasks:
1. **Input Stock Symbol**: use the provided stock symbol to query the tools and gather the relevant information.
2. **Analyze Data**: evaluate the results from the tools
3. **Summarize and Synthesize**: in particular, we need:
a) A summary of recent stock price movements, highlighting final available closing prices.
b) A summary of trends and potential resistance.
c) A summary of technical indicators (e.g., whether the stock is overbought or oversold).
d) A summary of forecasted returns and closing prices for the next 5 business days.
e) A summary of Financial health and performance based on financial metrics.
f) A final, conclusive synthesis that highlights key concerns and strenghts
g) Recommended action among following options:
- strong sell: if there are overwhelmingly bad signals
- sell: if there are some bad signals
- hold: there are either neutral signals, or good signals mixed with bad signals
- buy: there are some good signals
- strong buy: there are overwhelmingly good signals
### Constraints:
- Use only the data provided by the tools.
- If any tool fails to provide data, clearly state that in your summary.
- Try to provide a balanced synthesis based on the data provided by the tools.
- Avoid speculative language; focus on observable data and trends.
- Ensure that your response is objective, concise, and actionable.
"""
return stock_analyst_prompt.format(company=company)
def build_graph(
self
) -> StateGraph:
"""
Builds a state graph for stock analysis using a language model and financial tools.
This function constructs a state graph that processes stock analysis requests.
It defines a state schema, initializes tools for retrieving stock prices and
financial metrics, and binds these tools to a language model. The function
then adds nodes and edges to the graph, representing the sequence of operations
for analyzing stock data and generating analytical messages.
Returns:
StateGraph: A compiled state graph ready to process stock analysis tasks.
"""
class State(TypedDict):
messages: Annotated[list, add_messages]
stock: str
graph_builder = StateGraph(State)
tools = [get_stock_prices, get_financial_metrics]
llm = ChatOpenAI(model='gpt-4o-mini')
llm_with_tool = llm.bind_tools(
tools,
strict=True,
response_format=StockAnalysisResponse)
def stock_analyst(state: State):
messages = [
SystemMessage(content=self.get_prompt(state['stock'])),
] + state['messages']
return {
'messages': llm_with_tool.invoke(messages)
}
graph_builder.add_node('stock_analyst', stock_analyst)
graph_builder.add_edge(START, 'stock_analyst')
graph_builder.add_node(ToolNode(tools))
graph_builder.add_conditional_edges('stock_analyst', tools_condition)
graph_builder.add_edge('tools', 'stock_analyst')
# graph_builder.add_edge('stock_analyst', END)
graph = graph_builder.compile()
return graph
def draw_graph(
self,
graph
) -> None:
try:
display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
# This requires some extra dependencies and is optional
pass
def get_stock_analyses(
self,
ticker
) -> List[Union[HumanMessage, AIMessage, ToolMessage]]:
"""
Retrieves a list of stock analyses based on a given ticker symbol.
This function interacts with the state graph to stream events related
to stock analysis for the specified ticker. It sends a message asking
"Should I buy this stock?" and collects the resulting messages generated
by the graph, which contain stock suggestions.
Args:
ticker : str
The stock symbol (ticker) of the company to get suggestions for.
Returns:
List[Union[HumanMessage, AIMessage, ToolMessage]]: A list of messages of various types.
"""
events = self.graph.stream(
{
'messages':[('user', 'Should I buy this stock?')],
'stock': ticker
},
stream_mode='values'
)
# run the events and collect the current (last emitted) messages in a list
messages = []
for event in events:
if 'messages' in event:
messages.append(event['messages'][-1])
return messages
def get_formatted_stock_summary(
self,
ticker
) -> str:
"""
Retrieves analyses for a given stock ticker, syntheses information from messages, returns
a markdown formatted string containing a company's name, sector, and a summary of analyses.
Args:
ticker : str
The stock symbol (ticker) of the company to get the formatted summary for.
Returns:
str: A formatted string containing the company name, sector, and a summary of its stock analysis.
"""
messages = self.get_stock_analyses(ticker)
FA_str = messages[-2].model_dump()['content']
summary_str = messages[-1].model_dump()['content']
response_pretty = ''
try:
FA_dict = json.loads(FA_str)
response_pretty += f"**Company Name:** {FA_dict['Company Name']} \n"
response_pretty += f"**Sector:** {FA_dict['Sector']}\n\n"
except Exception as e:
response_pretty += f"**ticker**: {ticker}\n\n"
print(f'Error parsing the Financial Analysis response:\n{e}')
try:
summary_dict = json.loads(summary_str)
for key, value in summary_dict.items():
if key != 'stock':
pretty_key = key.replace('_', ' ').title()
response_pretty += f"**{pretty_key}**: {value}\n\n"
except Exception as e:
response_pretty += f'*An error occured stylizing the response, printing the raw response*:\n{summary_str}'
print(f'Error parsing summary response:\n{e}\n')
return response_pretty
if __name__ == "__main__":
stock_analyst = StockAnalyst(debug=False)
# messages = stock_analyst.get_stock_suggestion('GOOG')
# for message in messages:
# message.pretty_print()
print(stock_analyst.get_formatted_stock_summary('GOOG'))
|