CiscsoPonce commited on
Commit
109b51d
·
1 Parent(s): 52fef82

Whale_hunter_v3

Browse files
Files changed (1) hide show
  1. src/whale_hunter.py +87 -95
src/whale_hunter.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  import random
 
3
  from typing import TypedDict, Annotated, List, Union
4
  from langgraph.graph import StateGraph, END
5
  from langchain_core.messages import HumanMessage, SystemMessage
@@ -11,20 +12,22 @@ from src.finance_tools import check_financial_health
11
  from src.email_utils import send_email_report
12
  from src.agent import brave_market_search
13
 
14
- # --- 1. CONFIGURATION ---
15
- MAX_MARKET_CAP = 500_000_000 # < $500M
16
- MIN_MARKET_CAP = 1_000_000 # > $1M (Avoid Penny Stocks/Ghosts)
17
- MAX_RETRIES = 3 # Try 3 different stocks before giving up
 
18
 
19
  # --- 2. THE MEMORY (State) ---
20
  class AgentState(TypedDict):
 
21
  ticker: str
22
  company_name: str
23
  market_cap: float
24
  is_small_cap: bool
25
  financial_data: dict
26
  final_verdict: str
27
- retry_count: int # 🔄 Track how many times we tried
28
 
29
  llm = get_llm()
30
 
@@ -32,107 +35,109 @@ llm = get_llm()
32
 
33
  def scout_node(state):
34
  """
35
- 🕵️‍♂️ THE SCOUT
36
- Searches for a new target.
37
- If this is a retry, it changes the search query to find something fresh.
38
  """
 
39
  retries = state.get('retry_count', 0)
40
- print(f"🔭 Scouting... (Attempt {retries + 1}/{MAX_RETRIES + 1})")
41
-
42
- # 1. diverse queries to ensure we don't find the same stock twice
43
- queries = [
44
- "undervalued small cap stocks USA today",
45
- "top rated microcap stocks 2026",
46
- "insider buying small cap stocks this week",
47
- "deep value small cap stocks UK",
48
- "turnaround stocks under $500 million market cap",
49
- "net net stocks list 2026"
50
  ]
51
 
52
- # Pick a random query
53
- query = random.choice(queries)
54
  print(f" ↳ Query: '{query}'")
55
 
56
- # 2. Search
57
  search_results = brave_market_search(query)
58
 
59
- # 3. LLM Extraction
60
  extraction_prompt = f"""
61
  ROLE: Financial Data Extractor.
62
  INPUT: {search_results}
63
 
64
- TASK: Extract the single most interesting stock ticker.
65
- CONSTRAINT: Do NOT pick '{state.get('ticker', 'None')}'. Pick a DIFFERENT one if possible.
66
 
67
- OUTPUT: Just the ticker symbol (e.g., LMFA). No text.
68
  """
69
 
70
  try:
71
  if llm:
72
  ticker = llm.invoke(extraction_prompt).content.strip().upper()
73
  ticker = ticker.replace("$", "").replace("Ticker:", "").strip()
74
- # Remove junk length
75
- if len(ticker) > 6 or " " in ticker: ticker = "LMFA"
76
 
77
  print(f" 🎯 Target Acquired: {ticker}")
78
  return {"ticker": ticker, "retry_count": retries}
79
  else:
80
- return {"ticker": "LMFA", "retry_count": retries}
81
 
82
  except Exception as e:
83
  print(f" ❌ Extraction Error: {e}")
84
- return {"ticker": "LMFA", "retry_count": retries}
85
 
86
  def gatekeeper_node(state):
87
  """
88
  🛡️ THE STRICT GATEKEEPER
89
- Now rejects $0 Market Caps and forces a Retry.
90
  """
91
  ticker = state['ticker']
 
 
92
  print(f"⚖️ Weighing {ticker}...")
93
-
94
  try:
95
  stock = yf.Ticker(ticker)
 
 
96
  mkt_cap = stock.info.get('marketCap', 0)
97
  name = stock.info.get('shortName', ticker)
98
 
99
- # 🟢 STRICT LOGIC: Must be between $1M and $500M
100
  if MIN_MARKET_CAP < mkt_cap < MAX_MARKET_CAP:
101
- print(f"✅ {ticker} is a Valid Gem (${mkt_cap:,.0f}). Proceeding.")
102
  return {"market_cap": mkt_cap, "is_small_cap": True, "company_name": name}
103
-
104
  else:
105
- print(f"🚫 {ticker} Rejected. (Cap: ${mkt_cap:,.0f}). Requesting Retry.")
106
- # We return False so the graph loops back
107
  return {"market_cap": mkt_cap, "is_small_cap": False, "company_name": name}
108
 
109
- except Exception as e:
110
- print(f"❌ Gatekeeper Error: {e}")
111
  return {"is_small_cap": False, "market_cap": 0}
112
 
113
  def analyst_node(state):
114
  """
115
- 🧠 THE ANALYST
116
  """
117
  ticker = state['ticker']
118
- print(f"🧮 Analyzing {ticker}...")
119
-
120
  fin_data = check_financial_health(ticker)
121
- news = brave_market_search(f"{ticker} stock analysis")
122
 
123
  prompt = f"""
124
- Analyze {state['company_name']} ({ticker}).
125
- Market Cap: ${state.get('market_cap', 'N/A')}
126
- Financials: {fin_data.get('reason')}
127
- Metrics: {fin_data.get('metrics')}
128
- News: {news}
129
-
130
- Verdict: BUY / WATCH / AVOID.
131
- Thesis: 3 sentences max.
 
 
 
 
 
 
 
132
  """
133
 
134
  if llm:
135
- response = llm.invoke([SystemMessage(content="You are a value investor."), HumanMessage(content=prompt)])
136
  verdict = response.content
137
  else:
138
  verdict = f"Data: {fin_data.get('reason')}"
@@ -144,25 +149,23 @@ def email_node(state):
144
  📧 THE REPORTER
145
  """
146
  ticker = state.get('ticker', 'Unknown')
 
147
  verdict = state.get('final_verdict', 'No Verdict')
148
 
149
- # If we failed after 3 tries, send a failure report so we know.
150
  if not state.get('is_small_cap'):
151
- subject = "⚠️ Whale Hunter: Search Failed (3 Attempts)"
152
- html_body = f"<h1>Search Failed</h1><p>Tried 3 times. Last attempt: {ticker} (Cap: ${state.get('market_cap')})</p>"
153
- else:
154
- subject = f"🐳 Whale Hunter: {ticker} Analysis"
155
- html_body = f"""
156
- <h1>🌊 Whale Hunter Report: {ticker}</h1>
157
- <h3>Market Cap: ${state.get('market_cap', 0):,.0f}</h3>
158
- <hr>
159
- <p>{verdict.replace(chr(10), '<br>')}</p>
160
- <hr>
161
- <small>Generated by LangGraph Agent</small>
162
- """
163
-
164
- print(f"📨 Sending Email: {subject}")
165
-
166
  team = [
167
  {"name": "Cisco", "email": os.getenv("EMAIL_CISCO"), "key": os.getenv("RESEND_API_KEY_CISCO")},
168
  {"name": "Raul", "email": os.getenv("EMAIL_RAUL"), "key": os.getenv("RESEND_API_KEY_RAUL")},
@@ -177,10 +180,8 @@ def email_node(state):
177
 
178
  return {}
179
 
180
- # --- 4. THE GRAPH (Manager) ---
181
-
182
  workflow = StateGraph(AgentState)
183
-
184
  workflow.add_node("scout", scout_node)
185
  workflow.add_node("gatekeeper", gatekeeper_node)
186
  workflow.add_node("analyst", analyst_node)
@@ -188,30 +189,15 @@ workflow.add_node("email", email_node)
188
 
189
  workflow.set_entry_point("scout")
190
 
191
- # 🟢 THE LOOP LOGIC
192
  def check_status(state):
193
- if state['is_small_cap']:
194
- return "analyst" # ✅ Found one! Analyze it.
195
-
196
  if state['retry_count'] < MAX_RETRIES:
197
- # 🔄 Increment retry and LOOP BACK to scout
198
  state['retry_count'] += 1
199
  return "scout"
200
-
201
- return "email" # ❌ Give up and email failure report
202
 
203
  workflow.add_edge("scout", "gatekeeper")
204
-
205
- workflow.add_conditional_edges(
206
- "gatekeeper",
207
- check_status,
208
- {
209
- "analyst": "analyst",
210
- "scout": "scout", # 👈 The Loop
211
- "email": "email"
212
- }
213
- )
214
-
215
  workflow.add_edge("analyst", "email")
216
  workflow.add_edge("email", END)
217
 
@@ -219,10 +205,16 @@ app = workflow.compile()
219
 
220
  # 🟢 EXECUTION BLOCK
221
  if __name__ == "__main__":
222
- print("🚀 Starting Whale Hunter Agent (Sprint 7)...")
223
- try:
224
- # Initialize retry_count to 0
225
- result = app.invoke({"ticker": "", "retry_count": 0})
226
- print(" Mission Complete.")
227
- except Exception as e:
228
- print(f" CRITICAL FAILURE: {str(e)}")
 
 
 
 
 
 
 
1
  import os
2
  import random
3
+ import time
4
  from typing import TypedDict, Annotated, List, Union
5
  from langgraph.graph import StateGraph, END
6
  from langchain_core.messages import HumanMessage, SystemMessage
 
12
  from src.email_utils import send_email_report
13
  from src.agent import brave_market_search
14
 
15
+ # --- 1. CONFIGURATION (STRICT MODE) ---
16
+ # 📉 We are hunting MICRO-CAPS now.
17
+ MAX_MARKET_CAP = 300_000_000 # Limit: $300 Million (Strict)
18
+ MIN_MARKET_CAP = 20_000_000 # Min: $20 Million (Avoid total garbage)
19
+ MAX_RETRIES = 3 # Try harder to find a match
20
 
21
  # --- 2. THE MEMORY (State) ---
22
  class AgentState(TypedDict):
23
+ region: str
24
  ticker: str
25
  company_name: str
26
  market_cap: float
27
  is_small_cap: bool
28
  financial_data: dict
29
  final_verdict: str
30
+ retry_count: int
31
 
32
  llm = get_llm()
33
 
 
35
 
36
  def scout_node(state):
37
  """
38
+ 🕵️‍♂️ THE MICRO-CAP SCOUT
39
+ Searches specifically for 'Microcap' and 'Nano Cap' opportunities.
 
40
  """
41
+ region = state.get('region', 'USA')
42
  retries = state.get('retry_count', 0)
43
+
44
+ print(f"🔭 Scouting {region} Micro-Caps... (Attempt {retries + 1})")
45
+
46
+ # 🟢 NEW QUERIES: Explicitly ask for "Microcap" to avoid $1B companies
47
+ base_queries = [
48
+ f"undervalued microcap stocks {region} under $300m market cap",
49
+ f"profitable nano cap stocks {region} 2026",
50
+ f"hidden gem microcap stocks {region} with low float",
51
+ f"debt free microcap companies {region} high growth",
52
+ f"insider buying microcap stocks {region} this week"
53
  ]
54
 
55
+ query = random.choice(base_queries)
 
56
  print(f" ↳ Query: '{query}'")
57
 
 
58
  search_results = brave_market_search(query)
59
 
60
+ # LLM Extraction
61
  extraction_prompt = f"""
62
  ROLE: Financial Data Extractor.
63
  INPUT: {search_results}
64
 
65
+ TASK: Extract the single most interesting MICRO-CAP stock ticker.
66
+ CONSTRAINT: Must be listed in {region}. Ignore companies larger than $300M.
67
 
68
+ OUTPUT: Just the ticker symbol (e.g., LMFA, ABF.L). No text.
69
  """
70
 
71
  try:
72
  if llm:
73
  ticker = llm.invoke(extraction_prompt).content.strip().upper()
74
  ticker = ticker.replace("$", "").replace("Ticker:", "").strip()
75
+ if len(ticker) > 8 or " " in ticker: ticker = "NONE"
 
76
 
77
  print(f" 🎯 Target Acquired: {ticker}")
78
  return {"ticker": ticker, "retry_count": retries}
79
  else:
80
+ return {"ticker": "NONE", "retry_count": retries}
81
 
82
  except Exception as e:
83
  print(f" ❌ Extraction Error: {e}")
84
+ return {"ticker": "NONE", "retry_count": retries}
85
 
86
  def gatekeeper_node(state):
87
  """
88
  🛡️ THE STRICT GATEKEEPER
89
+ Rejects anything over $300M.
90
  """
91
  ticker = state['ticker']
92
+ if ticker == "NONE": return {"is_small_cap": False, "market_cap": 0}
93
+
94
  print(f"⚖️ Weighing {ticker}...")
 
95
  try:
96
  stock = yf.Ticker(ticker)
97
+
98
+ # Get Market Cap
99
  mkt_cap = stock.info.get('marketCap', 0)
100
  name = stock.info.get('shortName', ticker)
101
 
102
+ # 🟢 STRICT LOGIC: $20M - $300M Range
103
  if MIN_MARKET_CAP < mkt_cap < MAX_MARKET_CAP:
104
+ print(f"✅ {ticker} is a Micro-Cap (${mkt_cap:,.0f}). Accepted.")
105
  return {"market_cap": mkt_cap, "is_small_cap": True, "company_name": name}
 
106
  else:
107
+ print(f"🚫 {ticker} Rejected (${mkt_cap:,.0f}). Too Big/Small. Retry.")
 
108
  return {"market_cap": mkt_cap, "is_small_cap": False, "company_name": name}
109
 
110
+ except:
 
111
  return {"is_small_cap": False, "market_cap": 0}
112
 
113
  def analyst_node(state):
114
  """
115
+ 🧠 THE ANALYST (Graham Logic)
116
  """
117
  ticker = state['ticker']
 
 
118
  fin_data = check_financial_health(ticker)
119
+ news = brave_market_search(f"{ticker} stock investor analysis")
120
 
121
  prompt = f"""
122
+ Analyze {state['company_name']} ({ticker}) for a Value Investor.
123
+ Market Cap: ${state.get('market_cap', 0):,.0f} (Micro-Cap)
124
+
125
+ GRAHAM DATA:
126
+ {fin_data.get('metrics')}
127
+ Health Check: {fin_data.get('reason')}
128
+
129
+ MARKET NEWS:
130
+ {news}
131
+
132
+ TASK:
133
+ Write a concise thesis.
134
+ Does it pass the Graham Number test?
135
+
136
+ VERDICT: BUY / WATCH / AVOID.
137
  """
138
 
139
  if llm:
140
+ response = llm.invoke([SystemMessage(content="You are Benjamin Graham."), HumanMessage(content=prompt)])
141
  verdict = response.content
142
  else:
143
  verdict = f"Data: {fin_data.get('reason')}"
 
149
  📧 THE REPORTER
150
  """
151
  ticker = state.get('ticker', 'Unknown')
152
+ region = state.get('region', 'Global')
153
  verdict = state.get('final_verdict', 'No Verdict')
154
 
 
155
  if not state.get('is_small_cap'):
156
+ print(f"⚠️ No valid Micro-Cap found for {region} after retries.")
157
+ return {}
158
+
159
+ subject = f"🧬 Micro-Cap Hunter ({region}): {ticker}"
160
+ html_body = f"""
161
+ <h1>📍 Region: {region}</h1>
162
+ <h2>Ticker: {ticker}</h2>
163
+ <h3>Market Cap: ${state.get('market_cap', 0):,.0f}</h3>
164
+ <hr>
165
+ {verdict.replace(chr(10), '<br>')}
166
+ <hr>
167
+ """
168
+
 
 
169
  team = [
170
  {"name": "Cisco", "email": os.getenv("EMAIL_CISCO"), "key": os.getenv("RESEND_API_KEY_CISCO")},
171
  {"name": "Raul", "email": os.getenv("EMAIL_RAUL"), "key": os.getenv("RESEND_API_KEY_RAUL")},
 
180
 
181
  return {}
182
 
183
+ # --- 4. THE GRAPH ---
 
184
  workflow = StateGraph(AgentState)
 
185
  workflow.add_node("scout", scout_node)
186
  workflow.add_node("gatekeeper", gatekeeper_node)
187
  workflow.add_node("analyst", analyst_node)
 
189
 
190
  workflow.set_entry_point("scout")
191
 
 
192
  def check_status(state):
193
+ if state['is_small_cap']: return "analyst"
 
 
194
  if state['retry_count'] < MAX_RETRIES:
 
195
  state['retry_count'] += 1
196
  return "scout"
197
+ return END
 
198
 
199
  workflow.add_edge("scout", "gatekeeper")
200
+ workflow.add_conditional_edges("gatekeeper", check_status, {"analyst": "analyst", "scout": "scout", END: END})
 
 
 
 
 
 
 
 
 
 
201
  workflow.add_edge("analyst", "email")
202
  workflow.add_edge("email", END)
203
 
 
205
 
206
  # 🟢 EXECUTION BLOCK
207
  if __name__ == "__main__":
208
+ print("🚀 Starting Micro-Cap Hunter (Sprint 7)...")
209
+ regions = ["USA", "UK", "Canada", "Australia"]
210
+
211
+ for market in regions:
212
+ print(f"\n--- 🏁 Initiating Hunt for {market} ---")
213
+ try:
214
+ app.invoke({"region": market, "retry_count": 0, "ticker": ""})
215
+ print(f"✅ {market} Hunt Complete.")
216
+ time.sleep(2)
217
+ except Exception as e:
218
+ print(f"❌ Error in {market}: {e}")
219
+
220
+ print("\n🎉 Global Mission Complete.")