luisejdm commited on
Commit
c3bafdd
·
1 Parent(s): 95d7614

update some new tools

Browse files
Files changed (3) hide show
  1. app.py +3 -1
  2. prompts.py +104 -71
  3. tools.py +303 -228
app.py CHANGED
@@ -98,6 +98,7 @@ def build_demo() -> gr.Blocks:
98
  | Build a low-risk portfolio | "Create a minimum variance portfolio with Microsoft, Google, and Amazon" |
99
  | Maximize return for the risk taken | "Give me the best risk-return portfolio using Apple, Nvidia, and Meta" |
100
  | Reduce downside risk compared to the S&P500 | "Build a portfolio that minimizes losses relative to the S&P500 using these 5 stocks" |
 
101
  | Check Mexican CETES rates | "What's the 28-day CETES rate today?" |
102
  | Know monthly inflation in Mexico | "What was Mexico's monthly inflation last month?" |
103
  | Know annual inflation in Mexico | "What's the current annual inflation rate in Mexico?" |
@@ -106,7 +107,8 @@ def build_demo() -> gr.Blocks:
106
  | Know Mexico's central bank interest rate | "What is Mexico's target interest rate right now?" |
107
  | Get cross-currency exchange rates | "What's the current exchange rate for EUR/USD?" |
108
  | Analyze news sentiment for a stock | "What is the market sentiment around Tesla right now?" |
109
-
 
110
  """
111
  )
112
 
 
98
  | Build a low-risk portfolio | "Create a minimum variance portfolio with Microsoft, Google, and Amazon" |
99
  | Maximize return for the risk taken | "Give me the best risk-return portfolio using Apple, Nvidia, and Meta" |
100
  | Reduce downside risk compared to the S&P500 | "Build a portfolio that minimizes losses relative to the S&P500 using these 5 stocks" |
101
+ | Find economic data from the US (FRED) | "What was the unemployment rate in December 2024?" |
102
  | Check Mexican CETES rates | "What's the 28-day CETES rate today?" |
103
  | Know monthly inflation in Mexico | "What was Mexico's monthly inflation last month?" |
104
  | Know annual inflation in Mexico | "What's the current annual inflation rate in Mexico?" |
 
107
  | Know Mexico's central bank interest rate | "What is Mexico's target interest rate right now?" |
108
  | Get cross-currency exchange rates | "What's the current exchange rate for EUR/USD?" |
109
  | Analyze news sentiment for a stock | "What is the market sentiment around Tesla right now?" |
110
+ | Calculate the impact of inflation | "how much would inflation in mexico affect my 1000 pesos over 5 months?" |
111
+ | Make a fundamental analysis of a stock | "What is the fundamental analysis of Microsoft?" |
112
  """
113
  )
114
 
prompts.py CHANGED
@@ -1,3 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  DEFAULT_SYSTEM_PROMPT = """You are a financial data agent. You answer questions about companies and macroeconomic indicators using ONLY the results from your available tools. Never invent, estimate, or recall data from memory.
2
 
3
  STRICT RULES:
@@ -9,8 +25,8 @@ STRICT RULES:
9
 
10
  DATA SOURCE ROUTING — mandatory, no exceptions:
11
  - CETES, TIE, UDIs, tasa objetivo, inflacion Mexico, or ANY Mexican indicator → use the specific Banxico tool.
12
- Available Banxico tools: get_cetes_28, get_cetes_91, get_cetes_182, get_cetes_364, get_cetes_728,
13
- get_tie_28, get_tie_91, get_tie_182, get_target_interest_rate_mexico,
14
  get_mensual_inflation_mexico, get_inflation_mexico, get_udis.
15
  - Cross rates(e.g. EUR/USD, GBP/JPY, USD/CAD) → use get_exchange_rate(base, quote, date). (date is optional)
16
 
@@ -81,62 +97,28 @@ Returns: "Optimal weights for minimum target semivariance portfolio:
81
  Annualized volatility: <volatility>"
82
  Example call: ACTION: min_target_semivariance_portfolio(AAPL, MSFT, GOOGL)
83
 
84
- ### get_cetes_28(date)
85
- Description: Returns the CETES 28-day yield from Banxico.
86
- If no date is provided, returns the most recent observation.
87
- Pass a date in YYYY-MM-DD format to get the nearest available observation.
88
- Returns: "The CETES 28-day rate (<label>) is <value>% as of <DD/MM/YYYY>."
89
- Example calls: ACTION: get_cetes_28()
90
- ACTION: get_cetes_28(2024-01-15)
91
-
92
- ### get_cetes_91(date)
93
- Description: Returns the CETES 91-day yield from Banxico.
94
- If no date is provided, returns the most recent observation.
95
- Returns: "The CETES 91-day rate (<label>) is <value>% as of <DD/MM/YYYY>."
96
- Example calls: ACTION: get_cetes_91()
97
- ACTION: get_cetes_91(2024-01-15)
98
-
99
- ### get_cetes_182(date)
100
- Description: Returns the CETES 182-day yield from Banxico.
101
- If no date is provided, returns the most recent observation.
102
- Returns: "The CETES 182-day rate (<label>) is <value>% as of <DD/MM/YYYY>."
103
- Example calls: ACTION: get_cetes_182()
104
- ACTION: get_cetes_182(2024-01-15)
105
-
106
- ### get_cetes_364(date)
107
- Description: Returns the CETES 364-day yield from Banxico.
108
- If no date is provided, returns the most recent observation.
109
- Returns: "The CETES 364-day rate (<label>) is <value>% as of <DD/MM/YYYY>."
110
- Example calls: ACTION: get_cetes_364()
111
- ACTION: get_cetes_364(2024-01-15)
112
-
113
- ### get_cetes_728(date)
114
- Description: Returns the CETES 728-day yield from Banxico.
115
- If no date is provided, returns the most recent observation.
116
- Returns: "The CETES 728-day rate (<label>) is <value>% as of <DD/MM/YYYY>."
117
- Example calls: ACTION: get_cetes_728()
118
- ACTION: get_cetes_728(2024-01-15)
119
-
120
- ### get_tie_28(date)
121
- Description: Returns the TIE (Tasa de Interés de Equilibrio) 28-day rate from Banxico.
122
- If no date is provided, returns the most recent observation.
123
- Returns: "The TIE 28-day rate (<label>) is <value>% as of <DD/MM/YYYY>."
124
- Example calls: ACTION: get_tie_28()
125
- ACTION: get_tie_28(2024-01-15)
126
-
127
- ### get_tie_91(date)
128
- Description: Returns the TIE 91-day rate from Banxico.
129
- If no date is provided, returns the most recent observation.
130
- Returns: "The TIE 91-day rate (<label>) is <value>% as of <DD/MM/YYYY>."
131
- Example calls: ACTION: get_tie_91()
132
- ACTION: get_tie_91(2024-01-15)
133
-
134
- ### get_tie_182(date)
135
- Description: Returns the TIE 182-day rate from Banxico.
136
- If no date is provided, returns the most recent observation.
137
- Returns: "The TIE 182-day rate (<label>) is <value>% as of <DD/MM/YYYY>."
138
- Example calls: ACTION: get_tie_182()
139
- ACTION: get_tie_182(2024-01-15)
140
 
141
  ### get_target_interest_rate_mexico(date)
142
  Description: Returns the Banxico target interest rate (tasa objetivo).
@@ -192,6 +174,46 @@ Example calls: ACTION: get_news_sentiment(AAPL)
192
  ACTION: get_news_sentiment(TSLA)
193
  ACTION: get_news_sentiment(BIMBOA.MX)
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  ### respond_to_greeting()
196
  Description: Responds to user greetings with a friendly introduction about the agent.
197
  Use this when the user greets you or asks a general question like "Hi", "Hello", "What are you?".
@@ -228,7 +250,6 @@ User: What does Grupo México do?
228
  ACTION: get_company_profile(GMEXICOB.MX)
229
  After tool result: FINAL: Grupo México operates in the Basic Materials sector and Copper industry. The company is one of the largest mining groups in Latin America, focused on copper, silver, and zinc extraction.
230
 
231
-
232
  ACTION: get_company_profile(NVDA)
233
  After tool result: FINAL: Nvidia operates in the Technology sector and Semiconductors industry. The company designs GPUs and accelerated computing platforms for gaming, data centers, and artificial intelligence.
234
 
@@ -252,13 +273,29 @@ User: What is the market sentiment for Apple stock right now?
252
  ACTION: get_news_sentiment(AAPL)
253
  After tool result: FINAL: Sentiment analysis for Apple Inc. (AAPL) across 10 recent articles: Composite score: +0.1823 (POSITIVE). Top influencing headlines: [POSITIVE 88%] Apple reports record services revenue (CNBC) --- ...
254
 
255
- User: What is the most recent CETES 28-day rate?
256
- ACTION: get_cetes_28()
257
- After tool result: FINAL: The CETES 28-day rate (most recent) is 8.9900% as of 27/03/2025.
258
-
259
- User: What was the CETES 91-day rate in January 2024?
260
- ACTION: get_cetes_91(2024-01-15)
261
- After tool result: FINAL: The CETES 91-day rate nearest to January 15, 2024 was 11.3100% as of 11/01/2024.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
  User: What is the current Banxico target rate?
264
  ACTION: get_target_interest_rate_mexico()
@@ -272,10 +309,6 @@ User: What was the monthly inflation in Mexico in mid-2023?
272
  ACTION: get_mensual_inflation_mexico(2023-06-15)
273
  After tool result: FINAL: The monthly inflation rate in Mexico nearest to June 15, 2023 was 0.2200% as of 15/06/2023.
274
 
275
- User: What is the TIE 182-day rate?
276
- ACTION: get_tie_182()
277
- After tool result: FINAL: The TIE 182-day rate (most recent) is 9.4500% as of 15/04/2025.
278
-
279
  User: What is the current UDI value?
280
  ACTION: get_udis()
281
  After tool result: FINAL: The value of UDIs in Mexico (most recent) is 8.2341 MXN as of 30/04/2025.
@@ -298,4 +331,4 @@ ACTION: get_price_on_date(AAPL, March 2023) <- date must be in YYYY-MM-
298
  ACTION: min_variance_portfolio("AAPL, MSFT, GOOGL") <- never pack tickers into one string argument
299
  FINAL: Apple's price is around $210 <- invented value, no tool was called
300
  FINAL: The CETES rate is roughly 9% <- recalled from memory, no tool was called
301
- """
 
1
+ """
2
+ Prompt templates — update the system prompt here without touching agent logic.
3
+
4
+ HOW TO ADD A NEW TOOL TO THE PROMPT
5
+ -------------------------------------
6
+ 1. Add a new entry under AVAILABLE TOOLS following this block format:
7
+
8
+ ### tool_name(arg1, arg2, ...)
9
+ Description: What the tool does and when to use it.
10
+ Returns: What the tool output looks like.
11
+ Example call: ACTION: tool_name(ARG1)
12
+
13
+ 2. Add one correct and one incorrect example under EXAMPLES if useful.
14
+ 3. That's it — no other file needs to change.
15
+ """
16
+
17
  DEFAULT_SYSTEM_PROMPT = """You are a financial data agent. You answer questions about companies and macroeconomic indicators using ONLY the results from your available tools. Never invent, estimate, or recall data from memory.
18
 
19
  STRICT RULES:
 
25
 
26
  DATA SOURCE ROUTING — mandatory, no exceptions:
27
  - CETES, TIE, UDIs, tasa objetivo, inflacion Mexico, or ANY Mexican indicator → use the specific Banxico tool.
28
+ Available Banxico tools: get_cetes_rate,
29
+ get_tiie_rate, get_target_interest_rate_mexico,
30
  get_mensual_inflation_mexico, get_inflation_mexico, get_udis.
31
  - Cross rates(e.g. EUR/USD, GBP/JPY, USD/CAD) → use get_exchange_rate(base, quote, date). (date is optional)
32
 
 
97
  Annualized volatility: <volatility>"
98
  Example call: ACTION: min_target_semivariance_portfolio(AAPL, MSFT, GOOGL)
99
 
100
+ ### get_cetes_rate(term_days, date)
101
+ Description: Returns the CETES interest rate for a given term from Banxico.
102
+ term_days must be one of: 28, 91, 182, 364, 728.
103
+ If no date is provided, returns the most recent available rate.
104
+ Pass a date in YYYY-MM-DD format to get the nearest available rate.
105
+ Returns: "The CETES <term>-day rate (<label>) is <value>% as of <YYYY-MM-DD>."
106
+ Example calls: ACTION: get_cetes_rate(28)
107
+ ACTION: get_cetes_rate(91, 2024-06-01)
108
+ ACTION: get_cetes_rate(182)
109
+ ACTION: get_cetes_rate(364, 2023-01-15)
110
+ ACTION: get_cetes_rate(728)
111
+
112
+ ### get_tiie_rate(term_days, date)
113
+ Description: Returns the TIIE (Tasa de Interés Interbancaria de Equilibrio) rate
114
+ for a given term from Banxico.
115
+ term_days must be one of: 28, 91, 182.
116
+ If no date is provided, returns the most recent available rate.
117
+ Pass a date in YYYY-MM-DD format to get the nearest available rate.
118
+ Returns: "The TIIE <term>-day rate (<label>) is <value>% as of <YYYY-MM-DD>."
119
+ Example calls: ACTION: get_tiie_rate(28)
120
+ ACTION: get_tiie_rate(91, 2024-06-01)
121
+ ACTION: get_tiie_rate(182, 2023-01-15)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  ### get_target_interest_rate_mexico(date)
124
  Description: Returns the Banxico target interest rate (tasa objetivo).
 
174
  ACTION: get_news_sentiment(TSLA)
175
  ACTION: get_news_sentiment(BIMBOA.MX)
176
 
177
+ ### get_fundamental_analysis(ticker)
178
+ Description: Performs a quantitative fundamental analysis scorecard for a stock.
179
+ Evaluates 15 metrics across four categories:
180
+ - Valuation (P/E, P/B, EV/EBITDA, PEG) — sector-adjusted thresholds
181
+ - Profitability (ROE, ROA, Gross margin, Net margin)
182
+ - Financial Health (D/E ratio, Current ratio, Interest coverage, FCF yield)
183
+ - Growth (Revenue growth, Earnings growth, Dividend yield)
184
+ Each metric is scored 0 (weak), 1 (neutral/unavailable), or 2 (strong).
185
+ Composite score out of 30: >=70% → BUY | 40-69% → HOLD | <40% → SELL.
186
+ Apply the same exchange suffix rules as get_price_on_date.
187
+ Use this when the user asks for a fundamental analysis, valuation, financial
188
+ health overview, or a buy/sell/hold recommendation for a company.
189
+ Returns: Single-paragraph scorecard with per-metric values and scores, category subtotals,
190
+ composite score, percentage, and a BUY/HOLD/SELL recommendation with rationale.
191
+ Example calls: ACTION: get_fundamental_analysis(AAPL)
192
+ ACTION: get_fundamental_analysis(NVDA)
193
+ ACTION: get_fundamental_analysis(BIMBOA.MX)
194
+
195
+ ### calculate_inflation_impact(amount, months, annual_inflation_rate)
196
+ Description: Calculates the loss of purchasing power of a given amount of money
197
+ due to compound inflation over a number of months.
198
+ amount is the initial sum in pesos (or any currency).
199
+ months is the number of months to project forward.
200
+ annual_inflation_rate is the yearly inflation rate as a percentage (e.g. 4.5 for 4.5%).
201
+ Always call get_inflation_rate() first to obtain the current rate, then pass
202
+ its result as annual_inflation_rate into this function.
203
+ Returns: A sentence describing the effective value after inflation and the total purchasing power loss.
204
+ Example calls: ACTION: calculate_inflation_impact(1000, 5, 4.5)
205
+ ACTION: calculate_inflation_impact(5000, 12, 3.8)
206
+ ACTION: calculate_inflation_impact(250, 3, 5.1)
207
+
208
+ ### multiply(a, b)
209
+ Description: Multiplies two numbers together and returns the result.
210
+ Use this whenever a multiplication is needed mid-reasoning
211
+ instead of computing it yourself.
212
+ Returns: "The result of <a> × <b> is <result>."
213
+ Example calls: ACTION: multiply(12, 8)
214
+ ACTION: multiply(1053.5, 0.042)
215
+ ACTION: multiply(3, 1000000)
216
+
217
  ### respond_to_greeting()
218
  Description: Responds to user greetings with a friendly introduction about the agent.
219
  Use this when the user greets you or asks a general question like "Hi", "Hello", "What are you?".
 
250
  ACTION: get_company_profile(GMEXICOB.MX)
251
  After tool result: FINAL: Grupo México operates in the Basic Materials sector and Copper industry. The company is one of the largest mining groups in Latin America, focused on copper, silver, and zinc extraction.
252
 
 
253
  ACTION: get_company_profile(NVDA)
254
  After tool result: FINAL: Nvidia operates in the Technology sector and Semiconductors industry. The company designs GPUs and accelerated computing platforms for gaming, data centers, and artificial intelligence.
255
 
 
273
  ACTION: get_news_sentiment(AAPL)
274
  After tool result: FINAL: Sentiment analysis for Apple Inc. (AAPL) across 10 recent articles: Composite score: +0.1823 (POSITIVE). Top influencing headlines: [POSITIVE 88%] Apple reports record services revenue (CNBC) --- ...
275
 
276
+ User: Can you do a fundamental analysis of Apple?
277
+ ACTION: get_fundamental_analysis(AAPL)
278
+ After tool result: FINAL: <full scorecard output from tool>
279
+
280
+ User: Should I buy or sell Nvidia based on its fundamentals?
281
+ ACTION: get_fundamental_analysis(NVDA)
282
+ After tool result: FINAL: <full scorecard output from tool>
283
+
284
+ User: Give me a fundamental valuation of Grupo Bimbo.
285
+ ACTION: get_fundamental_analysis(BIMBOA.MX)
286
+ After tool result: FINAL: <full scorecard output from tool>
287
+
288
+ User: Is Tesla a good investment right now?
289
+ ACTION: get_fundamental_analysis(TSLA)
290
+ After tool result: FINAL: <full scorecard output from tool>
291
+
292
+ User: What is the 28-day CETES rate?
293
+ ACTION: get_cetes_rate(28)
294
+ After tool result: FINAL: The CETES 28-day rate (CETES28D) is 9.0000% as of 20/03/2025.
295
+
296
+ User: What is the current tiee rate for 91 days as of 20/03/2025?
297
+ ACTION: get_tiie_rate(91, 2025-03-20)
298
+ After tool result: FINAL: The TIIE 91-day rate (TIIE91D) is 9.5000% as of 20/03/2025.
299
 
300
  User: What is the current Banxico target rate?
301
  ACTION: get_target_interest_rate_mexico()
 
309
  ACTION: get_mensual_inflation_mexico(2023-06-15)
310
  After tool result: FINAL: The monthly inflation rate in Mexico nearest to June 15, 2023 was 0.2200% as of 15/06/2023.
311
 
 
 
 
 
312
  User: What is the current UDI value?
313
  ACTION: get_udis()
314
  After tool result: FINAL: The value of UDIs in Mexico (most recent) is 8.2341 MXN as of 30/04/2025.
 
331
  ACTION: min_variance_portfolio("AAPL, MSFT, GOOGL") <- never pack tickers into one string argument
332
  FINAL: Apple's price is around $210 <- invented value, no tool was called
333
  FINAL: The CETES rate is roughly 9% <- recalled from memory, no tool was called
334
+ """
tools.py CHANGED
@@ -13,7 +13,9 @@ import torch
13
  import torch.nn.functional as F
14
 
15
  import os
 
16
 
 
17
  BANXICO_TOKEN = os.getenv("BANXICO_TOKEN")
18
  HF_LOGIN_KEY = os.getenv("HF_LOGIN_KEY")
19
  if HF_LOGIN_KEY:
@@ -59,25 +61,25 @@ def build_default_tool_registry() -> ToolRegistry:
59
  @tool("get_price_on_date")
60
  def get_price_on_date(ticker, date=None):
61
  t = yf.Ticker(ticker)
62
-
63
  use_default_date = date is None
64
-
65
  if date is None:
66
  date = datetime.date.today()
67
  else:
68
  date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
69
-
70
- data = pd.DataFrame(t.history(start=date - datetime.timedelta(days=5), end=date + datetime.timedelta(days=5))['Close'])
71
  if data.empty:
72
  return f"No price data available for {t.ticker} around {date}."
73
-
74
  data['Date'] = data.index.date
75
  data['DateDiff'] = np.abs(data['Date'] - date)
76
  nearest_row = data.loc[data['DateDiff'].idxmin()]
77
  price = nearest_row['Close']
78
  actual_date = nearest_row['Date']
79
  official_name = t.info['longName']
80
-
81
  if use_default_date:
82
  return f"The last price of {official_name} ({t.ticker}) is ${price:.2f} as of {actual_date}."
83
  else:
@@ -160,115 +162,31 @@ def min_target_semivariance_portfolio(*tickers: str) -> str:
160
  f"Expected annual return: {(returns.mean() @ result.x * 252):.2%}\n"
161
  f"Annualized volatility: {(np.sqrt(result.x.T @ cov_matrix @ result.x) * np.sqrt(252)):.2%}"
162
  )
 
 
 
 
 
 
 
 
163
 
164
- @tool("get_cetes_28")
165
- def get_cetes_28(date: str | None = None) -> str:
166
- URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF43936/datos"
167
- headers = {
168
- "Bmx-Token": BANXICO_TOKEN,
169
- "Content-Type": "application/json",
170
- }
171
- try:
172
- response = requests.get(URL, headers=headers)
173
- response.raise_for_status()
174
-
175
- obs_list = response.json()["bmx"]["series"][0]["datos"]
176
-
177
- if date is None:
178
- obs = obs_list[-1]
179
- else:
180
- target = datetime.datetime.strptime(date, "%Y-%m-%d")
181
- obs = min(
182
- obs_list,
183
- key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
184
- )
185
-
186
- fecha = obs["fecha"]
187
- fecha = datetime.datetime.strptime(fecha, "%d/%m/%Y").strftime("%Y-%m-%d")
188
- value = float(obs["dato"])
189
- label = f"nearest to {date}" if date else "most recent"
190
- return f"The CETES 28-day rate ({label}) is {value:.4f}% as of {fecha}."
191
-
192
- except ValueError:
193
- return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
194
- except Exception as exc:
195
- return f"Error fetching CETES 28-day rate: {exc}"
196
-
197
- @tool("get_cetes_91")
198
- def get_cetes_91(date: str | None = None) -> str:
199
- URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF43939/datos"
200
- headers = {
201
- "Bmx-Token": BANXICO_TOKEN,
202
- "Content-Type": "application/json",
203
- }
204
- try:
205
- response = requests.get(URL, headers=headers)
206
- response.raise_for_status()
207
-
208
- obs_list = response.json()["bmx"]["series"][0]["datos"]
209
-
210
- if date is None:
211
- obs = obs_list[-1]
212
- else:
213
- target = datetime.datetime.strptime(date, "%Y-%m-%d")
214
- obs = min(
215
- obs_list,
216
- key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
217
- )
218
-
219
- fecha = obs["fecha"]
220
- fecha = datetime.datetime.strptime(fecha, "%d/%m/%Y").strftime("%Y-%m-%d")
221
- value = float(obs["dato"])
222
- label = f"nearest to {date}" if date else "most recent"
223
- return f"The CETES 91-day rate ({label}) is {value:.4f}% as of {fecha}."
224
 
225
- except ValueError:
226
- return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
227
- except Exception as exc:
228
- return f"Error fetching CETES 91-day rate: {exc}"
229
-
230
- @tool("get_cetes_182")
231
- def get_cetes_182(date: str | None = None) -> str:
232
- URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF43942/datos"
233
  headers = {
234
  "Bmx-Token": BANXICO_TOKEN,
235
  "Content-Type": "application/json",
236
  }
237
- try:
238
- response = requests.get(URL, headers=headers)
239
- response.raise_for_status()
240
 
241
- obs_list = response.json()["bmx"]["series"][0]["datos"]
242
-
243
- if date is None:
244
- obs = obs_list[-1]
245
- else:
246
- target = datetime.datetime.strptime(date, "%Y-%m-%d")
247
- obs = min(
248
- obs_list,
249
- key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
250
- )
251
-
252
- fecha = obs["fecha"]
253
- fecha = datetime.datetime.strptime(fecha, "%d/%m/%Y").strftime("%Y-%m-%d")
254
- value = float(obs["dato"])
255
- label = f"nearest to {date}" if date else "most recent"
256
- return f"The CETES 182-day rate ({label}) is {value:.4f}% as of {fecha}."
257
-
258
- except ValueError:
259
- return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
260
- except Exception as exc:
261
- return f"Error fetching CETES 182-day rate: {exc}"
262
-
263
- @tool("get_cetes_364")
264
- def get_cetes_364(date: str | None = None) -> str:
265
- URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF43945/datos"
266
- headers = {
267
- "Bmx-Token": BANXICO_TOKEN,
268
- "Content-Type": "application/json",
269
- }
270
  try:
271
- response = requests.get(URL, headers=headers)
272
  response.raise_for_status()
273
 
274
  obs_list = response.json()["bmx"]["series"][0]["datos"]
@@ -282,49 +200,16 @@ def get_cetes_364(date: str | None = None) -> str:
282
  key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
283
  )
284
 
285
- fecha = obs["fecha"]
286
- fecha = datetime.datetime.strptime(fecha, "%d/%m/%Y").strftime("%Y-%m-%d")
287
  value = float(obs["dato"])
288
  label = f"nearest to {date}" if date else "most recent"
289
- return f"The CETES 364-day rate ({label}) is {value:.4f}% as of {fecha}."
290
 
291
  except ValueError:
292
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
293
  except Exception as exc:
294
- return f"Error fetching CETES 364-day rate: {exc}"
295
-
296
- @tool("get_cetes_728")
297
- def get_cetes_728(date: str | None = None) -> str:
298
- URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF349785/datos"
299
- headers = {
300
- "Bmx-Token": BANXICO_TOKEN,
301
- "Content-Type": "application/json",
302
- }
303
- try:
304
- response = requests.get(URL, headers=headers)
305
- response.raise_for_status()
306
-
307
- obs_list = response.json()["bmx"]["series"][0]["datos"]
308
-
309
- if date is None:
310
- obs = obs_list[-1]
311
- else:
312
- target = datetime.datetime.strptime(date, "%Y-%m-%d")
313
- obs = min(
314
- obs_list,
315
- key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
316
- )
317
-
318
- fecha = obs["fecha"]
319
- fecha = datetime.datetime.strptime(fecha, "%d/%m/%Y").strftime("%Y-%m-%d")
320
- value = float(obs["dato"])
321
- label = f"nearest to {date}" if date else "most recent"
322
- return f"The CETES 728-day rate ({label}) is {value:.4f}% as of {fecha}."
323
 
324
- except ValueError:
325
- return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
326
- except Exception as exc:
327
- return f"Error fetching CETES 728-day rate: {exc}"
328
 
329
  @tool("get_mensual_inflation_mexico")
330
  def get_mensual_inflation_mexico(date: str | None = None) -> str:
@@ -353,12 +238,12 @@ def get_mensual_inflation_mexico(date: str | None = None) -> str:
353
  value = float(obs["dato"])
354
  label = f"nearest to {date}" if date else "most recent"
355
  return f"The monthly inflation rate in Mexico ({label}) is {value:.4f}% as of {fecha}."
356
-
357
  except ValueError:
358
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
359
  except Exception as exc:
360
  return f"Error fetching monthly inflation rate in Mexico: {exc}"
361
-
362
  @tool("get_inflation_mexico")
363
  def get_inflation_mexico(date: str | None = None) -> str:
364
  URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SP30577/datos"
@@ -391,7 +276,7 @@ def get_inflation_mexico(date: str | None = None) -> str:
391
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
392
  except Exception as exc:
393
  return f"Error fetching annual inflation rate in Mexico: {exc}"
394
-
395
  @tool("get_udis")
396
  def get_udis(date: str | None = None) -> str:
397
  URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SP68257/datos"
@@ -424,82 +309,33 @@ def get_udis(date: str | None = None) -> str:
424
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
425
  except Exception as exc:
426
  return f"Error fetching UDIs value in Mexico: {exc}"
 
 
 
 
 
 
427
 
428
- @tool("get_tie_28")
429
- def get_tie_28(date: str | None = None) -> str:
430
- URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF43783/datos"
431
- headers = {
432
- "Bmx-Token": BANXICO_TOKEN,
433
- "Content-Type": "application/json",
434
- }
435
- try:
436
- response = requests.get(URL, headers=headers)
437
- response.raise_for_status()
438
-
439
- obs_list = response.json()["bmx"]["series"][0]["datos"]
440
-
441
- if date is None:
442
- obs = obs_list[-1]
443
- else:
444
- target = datetime.datetime.strptime(date, "%Y-%m-%d")
445
- obs = min(
446
- obs_list,
447
- key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
448
- )
449
-
450
- fecha = obs["fecha"]
451
- fecha = datetime.datetime.strptime(fecha, "%d/%m/%Y").strftime("%Y-%m-%d")
452
- value = float(obs["dato"])
453
- label = f"nearest to {date}" if date else "most recent"
454
- return f"The TIE 28-day rate ({label}) is {value:.4f}% as of {fecha}."
455
-
456
- except ValueError:
457
- return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
458
- except Exception as exc:
459
- return f"Error fetching TIE 28-day rate: {exc}"
460
-
461
- @tool("get_tie_91")
462
- def get_tie_91(date: str | None = None) -> str:
463
- URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF43878/datos"
464
  headers = {
465
  "Bmx-Token": BANXICO_TOKEN,
466
  "Content-Type": "application/json",
467
  }
468
- try:
469
- response = requests.get(URL, headers=headers)
470
- response.raise_for_status()
471
 
472
- obs_list = response.json()["bmx"]["series"][0]["datos"]
473
-
474
- if date is None:
475
- obs = obs_list[-1]
476
- else:
477
- target = datetime.datetime.strptime(date, "%Y-%m-%d")
478
- obs = min(
479
- obs_list,
480
- key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
481
- )
482
-
483
- fecha = obs["fecha"]
484
- fecha = datetime.datetime.strptime(fecha, "%d/%m/%Y").strftime("%Y-%m-%d")
485
- value = float(obs["dato"])
486
- label = f"nearest to {date}" if date else "most recent"
487
- return f"The TIE 91-day rate ({label}) is {value:.4f}% as of {fecha}."
488
-
489
- except ValueError:
490
- return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
491
- except Exception as exc:
492
- return f"Error fetching TIE 91-day rate: {exc}"
493
-
494
- @tool("get_tie_182")
495
- def get_tie_182(date: str | None = None) -> str:
496
- URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SF111916/datos"
497
- headers = {
498
- "Bmx-Token": BANXICO_TOKEN,
499
- "Content-Type": "application/json",
500
- }
501
  try:
502
- response = requests.get(URL, headers=headers)
503
  response.raise_for_status()
504
 
505
  obs_list = response.json()["bmx"]["series"][0]["datos"]
@@ -513,16 +349,15 @@ def get_tie_182(date: str | None = None) -> str:
513
  key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
514
  )
515
 
516
- fecha = obs["fecha"]
517
- fecha = datetime.datetime.strptime(fecha, "%d/%m/%Y").strftime("%Y-%m-%d")
518
  value = float(obs["dato"])
519
  label = f"nearest to {date}" if date else "most recent"
520
- return f"The TIE 182-day rate ({label}) is {value:.4f}% as of {fecha}."
521
 
522
  except ValueError:
523
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
524
  except Exception as exc:
525
- return f"Error fetching TIE 182-day rate: {exc}"
526
 
527
  @tool("get_target_interest_rate_mexico")
528
  def get_target_interest_rate_mexico(date: str | None = None) -> str:
@@ -556,44 +391,44 @@ def get_target_interest_rate_mexico(date: str | None = None) -> str:
556
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
557
  except Exception as exc:
558
  return f"Error fetching target interest rate in Mexico: {exc}"
559
-
560
  @tool("get_exchange_rate")
561
  def get_exchange_rate(base: str, quote: str, date: str | None = None) -> str:
562
  base = base.strip().upper()
563
  quote = quote.strip().upper()
564
  ticker_symbol = f"{base}{quote}=X"
565
-
566
  try:
567
  if date is None:
568
  target_date = datetime.date.today()
569
  else:
570
  target_date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
571
-
572
  t = yf.Ticker(ticker_symbol)
573
  data = t.history(
574
  start=target_date - datetime.timedelta(days=7),
575
  end=target_date + datetime.timedelta(days=7),
576
  )
577
-
578
  if data.empty:
579
  return (
580
  f"No exchange rate data found for {base}/{quote} ({ticker_symbol}). "
581
  f"Verify that both currency codes are valid ISO 4217 codes."
582
  )
583
-
584
  data["Date"] = data.index.date
585
  data["DateDiff"] = data["Date"].apply(lambda d: abs((d - target_date).days))
586
  nearest = data.loc[data["DateDiff"].idxmin()]
587
  rate = nearest["Close"]
588
  actual_date = nearest["Date"]
589
  date_label = f"nearest to {date}" if date else "most recent"
590
- return f"The exchange rate for {base}/{quote} ({date_label}) is {rate:.6f} as of {actual_date}."
591
-
592
  except ValueError:
593
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
594
  except Exception as exc:
595
  return f"Error fetching exchange rate for {base}/{quote}: {exc}"
596
-
597
  def _get_news(ticker: str) -> list[dict]:
598
  t = yf.Ticker(ticker)
599
  news = t.news
@@ -627,6 +462,11 @@ _LABEL_TO_SCORE = {
627
  "negative": -1
628
  }
629
 
 
 
 
 
 
630
  def _bucket_label(score: float) -> str:
631
  if score > 0.15:
632
  return "positive"
@@ -635,7 +475,7 @@ def _bucket_label(score: float) -> str:
635
  return "neutral"
636
 
637
  def _recency_weights(pub_dates: list[str]) -> list[float]:
638
- decay = 0.01
639
  parsed = []
640
  for d in pub_dates:
641
  try:
@@ -659,6 +499,7 @@ def _recency_weights(pub_dates: list[str]) -> list[float]:
659
  return weights
660
 
661
  def _score_texts(texts: list[str]) -> list[dict]:
 
662
  tokenizer, model = _load_finbert()
663
  results = []
664
  with torch.no_grad():
@@ -683,6 +524,8 @@ def _score_texts(texts: list[str]) -> list[dict]:
683
 
684
  @tool("get_news_sentiment")
685
  def get_news_sentiment(ticker: str) -> str:
 
 
686
  articles = _get_news(ticker)
687
  comp_name = yf.Ticker(ticker).info.get("longName", ticker)
688
  if not articles:
@@ -729,6 +572,237 @@ def get_news_sentiment(ticker: str) -> str:
729
  f"Composite score: {composite:+.4f} ({label}). "
730
  f"Top influencing headlines: {top_headlines}"
731
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
 
733
  @tool("respond_to_greeting")
734
  def respond_to_greeting() -> str:
@@ -737,3 +811,4 @@ def respond_to_greeting() -> str:
737
  @tool("respond_no_available_tool")
738
  def respond_no_available_tool(tool_name: str) -> str:
739
  return f"Sorry, currently i'm capable of doing that. Check the list of avaiable tools for more information."
 
 
13
  import torch.nn.functional as F
14
 
15
  import os
16
+ from dotenv import load_dotenv
17
 
18
+ load_dotenv()
19
  BANXICO_TOKEN = os.getenv("BANXICO_TOKEN")
20
  HF_LOGIN_KEY = os.getenv("HF_LOGIN_KEY")
21
  if HF_LOGIN_KEY:
 
61
  @tool("get_price_on_date")
62
  def get_price_on_date(ticker, date=None):
63
  t = yf.Ticker(ticker)
64
+
65
  use_default_date = date is None
66
+
67
  if date is None:
68
  date = datetime.date.today()
69
  else:
70
  date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
71
+
72
+ data = pd.DataFrame(t.history(start=date - datetime.timedelta(days=5), end=date + datetime.timedelta(days=5))['Close'])
73
  if data.empty:
74
  return f"No price data available for {t.ticker} around {date}."
75
+
76
  data['Date'] = data.index.date
77
  data['DateDiff'] = np.abs(data['Date'] - date)
78
  nearest_row = data.loc[data['DateDiff'].idxmin()]
79
  price = nearest_row['Close']
80
  actual_date = nearest_row['Date']
81
  official_name = t.info['longName']
82
+
83
  if use_default_date:
84
  return f"The last price of {official_name} ({t.ticker}) is ${price:.2f} as of {actual_date}."
85
  else:
 
162
  f"Expected annual return: {(returns.mean() @ result.x * 252):.2%}\n"
163
  f"Annualized volatility: {(np.sqrt(result.x.T @ cov_matrix @ result.x) * np.sqrt(252)):.2%}"
164
  )
165
+
166
+ CETES_SERIES = {
167
+ 28: "SF43936",
168
+ 91: "SF43939",
169
+ 182: "SF43942",
170
+ 364: "SF43945",
171
+ 728: "SF349785",
172
+ }
173
 
174
+ @tool("get_cetes_rate")
175
+ def get_cetes_rate(term_days: int, date: str | None = None) -> str:
176
+ #valid days are the ones displayed above
177
+ if term_days not in CETES_SERIES:
178
+ valid = ", ".join(str(k) for k in CETES_SERIES)
179
+ return f"Invalid term '{term_days}'. Valid options are: {valid}."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
+ series_id = CETES_SERIES[term_days]
182
+ url = f"https://www.banxico.org.mx/SieAPIRest/service/v1/series/{series_id}/datos"
 
 
 
 
 
 
183
  headers = {
184
  "Bmx-Token": BANXICO_TOKEN,
185
  "Content-Type": "application/json",
186
  }
 
 
 
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  try:
189
+ response = requests.get(url, headers=headers)
190
  response.raise_for_status()
191
 
192
  obs_list = response.json()["bmx"]["series"][0]["datos"]
 
200
  key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
201
  )
202
 
203
+ fecha = datetime.datetime.strptime(obs["fecha"], "%d/%m/%Y").strftime("%Y-%m-%d")
 
204
  value = float(obs["dato"])
205
  label = f"nearest to {date}" if date else "most recent"
206
+ return f"The CETES {term_days}-day rate ({label}) is {value:.4f}% as of {fecha}."
207
 
208
  except ValueError:
209
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
210
  except Exception as exc:
211
+ return f"Error fetching CETES {term_days}-day rate: {exc}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
 
 
 
 
213
 
214
  @tool("get_mensual_inflation_mexico")
215
  def get_mensual_inflation_mexico(date: str | None = None) -> str:
 
238
  value = float(obs["dato"])
239
  label = f"nearest to {date}" if date else "most recent"
240
  return f"The monthly inflation rate in Mexico ({label}) is {value:.4f}% as of {fecha}."
241
+
242
  except ValueError:
243
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
244
  except Exception as exc:
245
  return f"Error fetching monthly inflation rate in Mexico: {exc}"
246
+
247
  @tool("get_inflation_mexico")
248
  def get_inflation_mexico(date: str | None = None) -> str:
249
  URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SP30577/datos"
 
276
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
277
  except Exception as exc:
278
  return f"Error fetching annual inflation rate in Mexico: {exc}"
279
+
280
  @tool("get_udis")
281
  def get_udis(date: str | None = None) -> str:
282
  URL = "https://www.banxico.org.mx/SieAPIRest/service/v1/series/SP68257/datos"
 
309
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
310
  except Exception as exc:
311
  return f"Error fetching UDIs value in Mexico: {exc}"
312
+
313
+ TIIE_SERIES = {
314
+ 28: "SF43783",
315
+ 91: "SF43878",
316
+ 182: "SF111916",
317
+ }
318
 
319
+ @tool("get_tiie_rate")
320
+ def get_tiie_rate(term_days: int, date: str | None = None) -> str:
321
+ """
322
+ Fetches the TIIE (Tasa de Interés Interbancaria de Equilibrio) rate
323
+ for a given term in days from Banxico.
324
+ Valid terms are: 28, 91, 182.
325
+ """
326
+ if term_days not in TIIE_SERIES:
327
+ valid = ", ".join(str(k) for k in TIIE_SERIES)
328
+ return f"Invalid term '{term_days}'. Valid options are: {valid}."
329
+
330
+ series_id = TIIE_SERIES[term_days]
331
+ url = f"https://www.banxico.org.mx/SieAPIRest/service/v1/series/{series_id}/datos"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  headers = {
333
  "Bmx-Token": BANXICO_TOKEN,
334
  "Content-Type": "application/json",
335
  }
 
 
 
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  try:
338
+ response = requests.get(url, headers=headers)
339
  response.raise_for_status()
340
 
341
  obs_list = response.json()["bmx"]["series"][0]["datos"]
 
349
  key=lambda o: abs(datetime.datetime.strptime(o["fecha"], "%d/%m/%Y") - target),
350
  )
351
 
352
+ fecha = datetime.datetime.strptime(obs["fecha"], "%d/%m/%Y").strftime("%Y-%m-%d")
 
353
  value = float(obs["dato"])
354
  label = f"nearest to {date}" if date else "most recent"
355
+ return f"The TIIE {term_days}-day rate ({label}) is {value:.4f}% as of {fecha}."
356
 
357
  except ValueError:
358
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
359
  except Exception as exc:
360
+ return f"Error fetching TIIE {term_days}-day rate: {exc}"
361
 
362
  @tool("get_target_interest_rate_mexico")
363
  def get_target_interest_rate_mexico(date: str | None = None) -> str:
 
391
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
392
  except Exception as exc:
393
  return f"Error fetching target interest rate in Mexico: {exc}"
394
+
395
  @tool("get_exchange_rate")
396
  def get_exchange_rate(base: str, quote: str, date: str | None = None) -> str:
397
  base = base.strip().upper()
398
  quote = quote.strip().upper()
399
  ticker_symbol = f"{base}{quote}=X"
400
+
401
  try:
402
  if date is None:
403
  target_date = datetime.date.today()
404
  else:
405
  target_date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
406
+
407
  t = yf.Ticker(ticker_symbol)
408
  data = t.history(
409
  start=target_date - datetime.timedelta(days=7),
410
  end=target_date + datetime.timedelta(days=7),
411
  )
412
+
413
  if data.empty:
414
  return (
415
  f"No exchange rate data found for {base}/{quote} ({ticker_symbol}). "
416
  f"Verify that both currency codes are valid ISO 4217 codes."
417
  )
418
+
419
  data["Date"] = data.index.date
420
  data["DateDiff"] = data["Date"].apply(lambda d: abs((d - target_date).days))
421
  nearest = data.loc[data["DateDiff"].idxmin()]
422
  rate = nearest["Close"]
423
  actual_date = nearest["Date"]
424
  date_label = f"nearest to {date}" if date else "most recent"
425
+ return f"The exchange rate for {base}/{quote} ({date_label}) is {rate:.6f} as of {actual_date}."
426
+
427
  except ValueError:
428
  return f"Invalid date format '{date}'. Please use YYYY-MM-DD (e.g. 2024-01-15)."
429
  except Exception as exc:
430
  return f"Error fetching exchange rate for {base}/{quote}: {exc}"
431
+
432
  def _get_news(ticker: str) -> list[dict]:
433
  t = yf.Ticker(ticker)
434
  news = t.news
 
462
  "negative": -1
463
  }
464
 
465
+ _SCORE_TO_LABEL = {
466
+ lambda s: s > 0.15: "positive",
467
+ lambda s: s < -0.15: "negative",
468
+ }
469
+
470
  def _bucket_label(score: float) -> str:
471
  if score > 0.15:
472
  return "positive"
 
475
  return "neutral"
476
 
477
  def _recency_weights(pub_dates: list[str]) -> list[float]:
478
+ decay = 0.01
479
  parsed = []
480
  for d in pub_dates:
481
  try:
 
499
  return weights
500
 
501
  def _score_texts(texts: list[str]) -> list[dict]:
502
+ """Returns a list of {label, confidence} dicts, one per input text."""
503
  tokenizer, model = _load_finbert()
504
  results = []
505
  with torch.no_grad():
 
524
 
525
  @tool("get_news_sentiment")
526
  def get_news_sentiment(ticker: str) -> str:
527
+ """Fetches recent news for a ticker and returns a FinBERT-based
528
+ sentiment score aggregated across all available headlines."""
529
  articles = _get_news(ticker)
530
  comp_name = yf.Ticker(ticker).info.get("longName", ticker)
531
  if not articles:
 
572
  f"Composite score: {composite:+.4f} ({label}). "
573
  f"Top influencing headlines: {top_headlines}"
574
  )
575
+
576
+ @tool("calculate_inflation_impact")
577
+ def calculate_inflation_impact(amount: float, months: int, annual_inflation_rate: float) -> str:
578
+ monthly_rate = (1 + annual_inflation_rate / 100) ** (1 / 12) - 1
579
+ future_equivalent = amount * (1 + monthly_rate) ** months
580
+ purchasing_power_loss = future_equivalent - amount
581
+ effective_value = amount - purchasing_power_loss
582
+
583
+ return (
584
+ f"With an annual inflation rate of {annual_inflation_rate:.2f}%, "
585
+ f"{amount:.2f} pesos today will have the purchasing power of "
586
+ f"{effective_value:.2f} pesos after {months} month(s). "
587
+ f"That is a loss of {purchasing_power_loss:.2f} pesos in real value."
588
+ )
589
+
590
+ @tool("multiply")
591
+ def multiply(a: float, b: float) -> str:
592
+ result = a * b
593
+ return f"The result of {a} × {b} is {result}."
594
+
595
+ # --------------- sector-adjusted valuation thresholds -------------------------
596
+ # Tuple layout: (pe_strong, pe_weak, pb_strong, pb_weak, ev_ebitda_strong, ev_ebitda_weak)
597
+ # "strong" means the value that earns the maximum score of 2.
598
+ # "weak" means the value that earns the minimum score of 0.
599
+ # Values between the two thresholds score 1 (neutral).
600
+ _SECTOR_VALUATION_THRESHOLDS: dict[str, tuple] = {
601
+ "technology": (25, 45, 4.0, 10.0, 15, 30),
602
+ "healthcare": (20, 35, 3.0, 8.0, 14, 25),
603
+ "financial-services": (12, 20, 1.0, 2.5, 10, 18),
604
+ "consumer-cyclical": (18, 30, 2.5, 6.0, 12, 22),
605
+ "consumer-defensive": (18, 28, 3.0, 6.0, 12, 20),
606
+ "energy": (10, 20, 1.5, 3.0, 6, 14),
607
+ "basic-materials": (12, 22, 1.5, 3.5, 8, 16),
608
+ "industrials": (18, 30, 2.5, 5.0, 12, 20),
609
+ "real-estate": (30, 55, 1.5, 3.5, 18, 30),
610
+ "utilities": (15, 25, 1.5, 3.0, 10, 18),
611
+ "default": (18, 35, 2.5, 6.0, 12, 22),
612
+ }
613
+
614
+
615
+ def _valuation_thresholds(sector_key: str | None) -> tuple:
616
+ key = (sector_key or "").lower()
617
+ return _SECTOR_VALUATION_THRESHOLDS.get(key, _SECTOR_VALUATION_THRESHOLDS["default"])
618
+
619
+
620
+ def _score_metric(value: float, strong_threshold: float, weak_threshold: float,
621
+ lower_is_better: bool = True) -> int:
622
+ """
623
+ Scores a single metric on a 0–2 scale.
624
+
625
+ For lower_is_better metrics (P/E, D/E, EV/EBITDA …):
626
+ value <= strong_threshold → 2
627
+ value >= weak_threshold → 0
628
+ in between → 1
629
+
630
+ For higher_is_better metrics (ROE, margins, FCF yield …):
631
+ value >= strong_threshold → 2
632
+ value <= weak_threshold → 0
633
+ in between → 1
634
+ """
635
+ if lower_is_better:
636
+ if value <= strong_threshold:
637
+ return 2
638
+ if value >= weak_threshold:
639
+ return 0
640
+ return 1
641
+ else:
642
+ if value >= strong_threshold:
643
+ return 2
644
+ if value <= weak_threshold:
645
+ return 0
646
+ return 1
647
+
648
+
649
+ @tool("get_fundamental_analysis")
650
+ def get_fundamental_analysis(ticker: str) -> str:
651
+ """
652
+ Performs a quantitative fundamental analysis scorecard for a given ticker.
653
+
654
+ Evaluates 15 metrics across four categories:
655
+ - Valuation (P/E, P/B, EV/EBITDA, PEG) max 8 pts
656
+ - Profitability (ROE, ROA, Gross margin, Net margin) max 8 pts
657
+ - Financial Health (D/E, Current ratio, IC, FCF yield) max 8 pts
658
+ - Growth (Revenue growth, Earnings growth, Div yield) max 6 pts
659
+ ──────────
660
+ TOTAL max 30 pts
661
+
662
+ Scoring per metric: 2 = strong, 1 = neutral / data unavailable, 0 = weak.
663
+ Valuation thresholds are sector-adjusted via yfinance sectorKey.
664
+ Composite: ≥70% → BUY | 40–69% → HOLD | <40% → SELL.
665
+
666
+ Apply the same exchange suffix rules as get_price_on_date (e.g. BIMBOA.MX).
667
+ """
668
+ t = yf.Ticker(ticker)
669
+ info = t.info
670
+
671
+ company_name = info.get("longName", ticker)
672
+ sector_key = info.get("sectorKey", None)
673
+ sector_label = info.get("sector", "Unknown sector")
674
+
675
+ pe_s, pe_w, pb_s, pb_w, ev_s, ev_w = _valuation_thresholds(sector_key)
676
+
677
+ def safe(key: str, scale: float = 1.0):
678
+ """Returns (scaled_value, is_available). Missing or non-numeric → (None, False)."""
679
+ raw = info.get(key)
680
+ if raw is None or not isinstance(raw, (int, float)):
681
+ return None, False
682
+ return raw * scale, True
683
+
684
+ # ── Valuation (max 8 pts) ──────────────────────────────────────────────────
685
+ pe, pe_ok = safe("trailingPE")
686
+ pb, pb_ok = safe("priceToBook")
687
+ ev_ebitda, ev_ok = safe("enterpriseToEbitda")
688
+ peg, peg_ok = safe("pegRatio")
689
+
690
+ pe_score = _score_metric(pe, pe_s, pe_w, lower_is_better=True) if pe_ok else 1
691
+ pb_score = _score_metric(pb, pb_s, pb_w, lower_is_better=True) if pb_ok else 1
692
+ ev_score = _score_metric(ev_ebitda, ev_s, ev_w, lower_is_better=True) if ev_ok else 1
693
+ peg_score = _score_metric(peg, 1.0, 2.0, lower_is_better=True) if peg_ok else 1
694
+
695
+ valuation_score = pe_score + pb_score + ev_score + peg_score # max 8
696
+
697
+ # ── Profitability (max 8 pts) ──────────────────────────────────────────────
698
+ roe, roe_ok = safe("returnOnEquity", scale=100)
699
+ roa, roa_ok = safe("returnOnAssets", scale=100)
700
+ gross_m, gm_ok = safe("grossMargins", scale=100)
701
+ net_m, nm_ok = safe("profitMargins", scale=100)
702
+
703
+ roe_score = _score_metric(roe, 15.0, 8.0, lower_is_better=False) if roe_ok else 1
704
+ roa_score = _score_metric(roa, 5.0, 2.0, lower_is_better=False) if roa_ok else 1
705
+ gm_score = _score_metric(gross_m, 40.0, 20.0, lower_is_better=False) if gm_ok else 1
706
+ nm_score = _score_metric(net_m, 10.0, 3.0, lower_is_better=False) if nm_ok else 1
707
+
708
+ profit_score = roe_score + roa_score + gm_score + nm_score # max 8
709
+
710
+ # ── Financial Health (max 8 pts) ───────────────────────────────────────────
711
+ de, de_ok = safe("debtToEquity")
712
+ cr, cr_ok = safe("currentRatio")
713
+ ebitda, ebit_ok = safe("ebitda")
714
+ int_exp, ie_ok = safe("interestExpense")
715
+ fcf, fcf_ok = safe("freeCashflow")
716
+ mktcap, mc_ok = safe("marketCap")
717
+
718
+ # yfinance returns D/E as a percentage (e.g. 150 means 1.50); normalise to ratio.
719
+ de_adj = de / 100.0 if de_ok else None
720
+ de_score = _score_metric(de_adj, 0.5, 1.5, lower_is_better=True) if de_ok else 1
721
+
722
+ cr_score = _score_metric(cr, 2.0, 1.0, lower_is_better=False) if cr_ok else 1
723
+
724
+ # Interest coverage = EBITDA / |interest expense|; higher is better.
725
+ if ebit_ok and ie_ok and int_exp != 0:
726
+ ic = abs(ebitda) / abs(int_exp)
727
+ ic_score = _score_metric(ic, 5.0, 2.0, lower_is_better=False)
728
+ else:
729
+ ic = None
730
+ ic_score = 1
731
+
732
+ # FCF yield = FCF / market cap (%); >5% strong, <0% weak.
733
+ if fcf_ok and mc_ok and mktcap > 0:
734
+ fcf_yield = (fcf / mktcap) * 100
735
+ fcf_score = _score_metric(fcf_yield, 5.0, 0.0, lower_is_better=False)
736
+ else:
737
+ fcf_yield = None
738
+ fcf_score = 1
739
+
740
+ health_score = de_score + cr_score + ic_score + fcf_score # max 8
741
+
742
+ # ── Growth (max 6 pts) ─────────────────────────────────────────────────────
743
+ rev_g, rg_ok = safe("revenueGrowth", scale=100)
744
+ earn_g, eg_ok = safe("earningsGrowth", scale=100)
745
+ div_y, dy_ok = safe("dividendYield", scale=100)
746
+
747
+ rev_score = _score_metric(rev_g, 10.0, 0.0, lower_is_better=False) if rg_ok else 1
748
+ earn_score = _score_metric(earn_g, 10.0, 0.0, lower_is_better=False) if eg_ok else 1
749
+
750
+ # Dividend yield: 2–5.5% is the ideal income range.
751
+ # Below 0.5% is neutral (growth company, no penalty). Above 6% may signal distress.
752
+ if dy_ok:
753
+ if 2.0 <= div_y <= 5.5:
754
+ div_score = 2
755
+ elif div_y > 6.0 or div_y < 0.5:
756
+ div_score = 0
757
+ else:
758
+ div_score = 1
759
+ else:
760
+ div_score = 1 # no dividend data → neutral
761
+
762
+ growth_score = rev_score + earn_score + div_score # max 6
763
+
764
+ # ── Composite & recommendation ─────────────────────────────────────────────
765
+ MAX_SCORE = 30
766
+ composite = valuation_score + profit_score + health_score + growth_score
767
+ pct = composite / MAX_SCORE
768
+
769
+ if pct >= 0.70:
770
+ recommendation = "BUY"
771
+ rationale = "the company scores strongly across most fundamental dimensions"
772
+ elif pct >= 0.40:
773
+ recommendation = "HOLD"
774
+ rationale = "the fundamentals are mixed with no compelling entry or exit signal"
775
+ else:
776
+ recommendation = "SELL"
777
+ rationale = "the company shows material weakness across multiple fundamental dimensions"
778
+
779
+ def fmt(value, decimals: int = 2, suffix: str = "") -> str:
780
+ return "N/A" if value is None else f"{value:.{decimals}f}{suffix}"
781
+
782
+ return (
783
+ f"Fundamental analysis scorecard for {company_name} ({ticker.upper()}) | Sector: {sector_label}. "
784
+ f"VALUATION ({valuation_score}/8): "
785
+ f"P/E {fmt(pe)}x [score {pe_score}/2], "
786
+ f"P/B {fmt(pb)}x [score {pb_score}/2], "
787
+ f"EV/EBITDA {fmt(ev_ebitda)}x [score {ev_score}/2], "
788
+ f"PEG {fmt(peg)} [score {peg_score}/2]. "
789
+ f"PROFITABILITY ({profit_score}/8): "
790
+ f"ROE {fmt(roe)}% [score {roe_score}/2], "
791
+ f"ROA {fmt(roa)}% [score {roa_score}/2], "
792
+ f"Gross margin {fmt(gross_m)}% [score {gm_score}/2], "
793
+ f"Net margin {fmt(net_m)}% [score {nm_score}/2]. "
794
+ f"FINANCIAL HEALTH ({health_score}/8): "
795
+ f"D/E {fmt(de_adj)} [score {de_score}/2], "
796
+ f"Current ratio {fmt(cr)} [score {cr_score}/2], "
797
+ f"Interest coverage {fmt(ic)}x [score {ic_score}/2], "
798
+ f"FCF yield {fmt(fcf_yield)}% [score {fcf_score}/2]. "
799
+ f"GROWTH ({growth_score}/6): "
800
+ f"Revenue growth {fmt(rev_g)}% [score {rev_score}/2], "
801
+ f"Earnings growth {fmt(earn_g)}% [score {earn_score}/2], "
802
+ f"Dividend yield {fmt(div_y)}% [score {div_score}/2]. "
803
+ f"COMPOSITE SCORE: {composite}/{MAX_SCORE} ({pct:.0%}). "
804
+ f"RECOMMENDATION: {recommendation} — {rationale}."
805
+ )
806
 
807
  @tool("respond_to_greeting")
808
  def respond_to_greeting() -> str:
 
811
  @tool("respond_no_available_tool")
812
  def respond_no_available_tool(tool_name: str) -> str:
813
  return f"Sorry, currently i'm capable of doing that. Check the list of avaiable tools for more information."
814
+