CiscsoPonce commited on
Commit
a2930e5
ยท
1 Parent(s): 82b6633

Whale hunter_v2

Browse files
Files changed (2) hide show
  1. .github/workflows/hunter.yml +1 -0
  2. src/whale_hunter.py +88 -83
.github/workflows/hunter.yml CHANGED
@@ -9,6 +9,7 @@ on:
9
  jobs:
10
  global-hunt:
11
  runs-on: ubuntu-latest
 
12
  steps:
13
  - uses: actions/checkout@v3
14
 
 
9
  jobs:
10
  global-hunt:
11
  runs-on: ubuntu-latest
12
+ timeout-minutes: 5 # ๐Ÿ‘ˆ MAKE SURE THIS IS HERE
13
  steps:
14
  - uses: actions/checkout@v3
15
 
src/whale_hunter.py CHANGED
@@ -12,13 +12,13 @@ from src.finance_tools import check_financial_health
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
@@ -31,19 +31,18 @@ class AgentState(TypedDict):
31
 
32
  llm = get_llm()
33
 
34
- # --- 3. THE WORKERS (Nodes) ---
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",
@@ -51,12 +50,14 @@ def scout_node(state):
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.
@@ -72,82 +73,75 @@ def scout_node(state):
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
  """
90
  ticker = state['ticker']
91
- current_retries = state.get('retry_count', 0) # Get current count
92
 
93
- # Fail-safe for "NONE" ticker
94
- if ticker == "NONE":
 
95
  return {
96
  "is_small_cap": False,
97
- "market_cap": 0,
98
- "retry_count": current_retries + 1 # ๐Ÿ‘ˆ Increment on failure
99
  }
100
 
101
- print(f"โš–๏ธ Weighing {ticker}...")
 
102
  try:
103
  stock = yf.Ticker(ticker)
104
  mkt_cap = stock.info.get('marketCap', 0)
105
  name = stock.info.get('shortName', ticker)
106
 
107
- # ๐ŸŸข STRICT LOGIC
108
  if MIN_MARKET_CAP < mkt_cap < MAX_MARKET_CAP:
109
- print(f"โœ… {ticker} is a Micro-Cap (${mkt_cap:,.0f}). Accepted.")
110
  return {"market_cap": mkt_cap, "is_small_cap": True, "company_name": name}
111
  else:
112
- print(f"๐Ÿšซ {ticker} Rejected (${mkt_cap:,.0f}). Retry.")
113
  return {
114
  "market_cap": mkt_cap,
115
  "is_small_cap": False,
116
- "company_name": name,
117
- "retry_count": current_retries + 1 # ๐Ÿ‘ˆ INCREMENT HERE!
118
  }
119
 
120
- except:
 
121
  return {
122
  "is_small_cap": False,
123
- "market_cap": 0,
124
- "retry_count": current_retries + 1 # ๐Ÿ‘ˆ Increment on error
125
  }
126
 
127
  def analyst_node(state):
128
  """
129
- ๐Ÿง  THE ANALYST (Graham Logic)
130
  """
131
  ticker = state['ticker']
 
 
132
  fin_data = check_financial_health(ticker)
133
- news = brave_market_search(f"{ticker} stock investor analysis")
134
 
135
  prompt = f"""
136
- Analyze {state['company_name']} ({ticker}) for a Value Investor.
137
- Market Cap: ${state.get('market_cap', 0):,.0f} (Micro-Cap)
138
-
139
- GRAHAM DATA:
140
- {fin_data.get('metrics')}
141
- Health Check: {fin_data.get('reason')}
142
-
143
- MARKET NEWS:
144
- {news}
145
-
146
- TASK:
147
- Write a concise thesis.
148
- Does it pass the Graham Number test?
149
-
150
- VERDICT: BUY / WATCH / AVOID.
151
  """
152
 
153
  if llm:
@@ -160,25 +154,34 @@ def analyst_node(state):
160
 
161
  def email_node(state):
162
  """
163
- ๐Ÿ“ง THE REPORTER
164
  """
165
- ticker = state.get('ticker', 'Unknown')
166
  region = state.get('region', 'Global')
 
167
  verdict = state.get('final_verdict', 'No Verdict')
168
 
 
169
  if not state.get('is_small_cap'):
170
- print(f"โš ๏ธ No valid Micro-Cap found for {region} after retries.")
171
- return {}
172
-
173
- subject = f"๐Ÿงฌ Micro-Cap Hunter ({region}): {ticker}"
174
- html_body = f"""
175
- <h1>๐Ÿ“ Region: {region}</h1>
176
- <h2>Ticker: {ticker}</h2>
177
- <h3>Market Cap: ${state.get('market_cap', 0):,.0f}</h3>
178
- <hr>
179
- {verdict.replace(chr(10), '<br>')}
180
- <hr>
181
- """
 
 
 
 
 
 
 
 
182
 
183
  team = [
184
  {"name": "Cisco", "email": os.getenv("EMAIL_CISCO"), "key": os.getenv("RESEND_API_KEY_CISCO")},
@@ -204,19 +207,23 @@ workflow.add_node("email", email_node)
204
  workflow.set_entry_point("scout")
205
 
206
  def check_status(state):
207
- # If we found a gem, go to Analyst
208
- if state['is_small_cap']:
209
  return "analyst"
210
 
211
- # If we failed but have retries left, Loop back
212
- if state['retry_count'] <= MAX_RETRIES:
213
- return "scout"
214
 
215
- # If we ran out of retries, Give up
216
- return END
 
 
217
 
218
  workflow.add_edge("scout", "gatekeeper")
219
- workflow.add_conditional_edges("gatekeeper", check_status, {"analyst": "analyst", "scout": "scout", END: END})
 
 
220
  workflow.add_edge("analyst", "email")
221
  workflow.add_edge("email", END)
222
 
@@ -224,16 +231,14 @@ app = workflow.compile()
224
 
225
  # ๐ŸŸข EXECUTION BLOCK
226
  if __name__ == "__main__":
227
- print("๐Ÿš€ Starting Micro-Cap Hunter (Sprint 7)...")
228
  regions = ["USA", "UK", "Canada", "Australia"]
229
 
230
  for market in regions:
231
- print(f"\n--- ๐Ÿ Initiating Hunt for {market} ---")
232
  try:
 
233
  app.invoke({"region": market, "retry_count": 0, "ticker": ""})
234
- print(f"โœ… {market} Hunt Complete.")
235
- time.sleep(2)
236
  except Exception as e:
237
- print(f"โŒ Error in {market}: {e}")
238
-
239
- print("\n๐ŸŽ‰ Global Mission Complete.")
 
12
  from src.email_utils import send_email_report
13
  from src.agent import brave_market_search
14
 
15
+ # --- 1. CONFIGURATION ---
16
+ # ๐Ÿ“‰ MICRO-CAP SETTINGS
17
+ MAX_MARKET_CAP = 300_000_000 # Max: $300 Million
18
+ MIN_MARKET_CAP = 10_000_000 # Min: $10 Million (Avoid shells)
19
+ MAX_RETRIES = 2 # 2 Retries = 3 Total Attempts per region
20
 
21
+ # --- 2. THE MEMORY ---
22
  class AgentState(TypedDict):
23
  region: str
24
  ticker: str
 
31
 
32
  llm = get_llm()
33
 
34
+ # --- 3. THE WORKERS ---
35
 
36
  def scout_node(state):
37
  """
38
+ ๐Ÿ•ต๏ธโ€โ™‚๏ธ THE SCOUT: Finds a ticker.
 
39
  """
40
  region = state.get('region', 'USA')
41
  retries = state.get('retry_count', 0)
42
 
43
+ print(f"\n๐Ÿ”ญ [Attempt {retries+1}/{MAX_RETRIES+1}] Scouting {region} Micro-Caps...")
44
 
45
+ # Randomize query to find fresh targets
46
  base_queries = [
47
  f"undervalued microcap stocks {region} under $300m market cap",
48
  f"profitable nano cap stocks {region} 2026",
 
50
  f"debt free microcap companies {region} high growth",
51
  f"insider buying microcap stocks {region} this week"
52
  ]
 
53
  query = random.choice(base_queries)
 
 
 
54
 
55
+ try:
56
+ search_results = brave_market_search(query)
57
+ except Exception as e:
58
+ print(f" โŒ Search Error: {e}")
59
+ return {"ticker": "NONE"}
60
+
61
  # LLM Extraction
62
  extraction_prompt = f"""
63
  ROLE: Financial Data Extractor.
 
73
  if llm:
74
  ticker = llm.invoke(extraction_prompt).content.strip().upper()
75
  ticker = ticker.replace("$", "").replace("Ticker:", "").strip()
76
+ # Clean junk
77
  if len(ticker) > 8 or " " in ticker: ticker = "NONE"
78
 
79
+ print(f" ๐ŸŽฏ Target: {ticker}")
80
+ return {"ticker": ticker}
81
  else:
82
+ return {"ticker": "NONE"}
83
+ except:
84
+ return {"ticker": "NONE"}
 
 
85
 
86
  def gatekeeper_node(state):
87
  """
88
+ ๐Ÿ›ก๏ธ THE GATEKEEPER: Filters by size and manages the Retry Counter.
89
  """
90
  ticker = state['ticker']
91
+ current_retries = state.get('retry_count', 0)
92
 
93
+ # 1. Check for Invalid Ticker
94
+ if ticker == "NONE":
95
+ print(f" ๐Ÿšซ No valid ticker found. Incrementing Retry.")
96
  return {
97
  "is_small_cap": False,
98
+ "market_cap": 0,
99
+ "retry_count": current_retries + 1 # ๐Ÿ‘ˆ CRITICAL FIX
100
  }
101
 
102
+ # 2. Check Market Cap
103
+ print(f" โš–๏ธ Weighing {ticker}...")
104
  try:
105
  stock = yf.Ticker(ticker)
106
  mkt_cap = stock.info.get('marketCap', 0)
107
  name = stock.info.get('shortName', ticker)
108
 
 
109
  if MIN_MARKET_CAP < mkt_cap < MAX_MARKET_CAP:
110
+ print(f" โœ… {ticker} Accepted (${mkt_cap:,.0f}).")
111
  return {"market_cap": mkt_cap, "is_small_cap": True, "company_name": name}
112
  else:
113
+ print(f" ๐Ÿšซ {ticker} Rejected (${mkt_cap:,.0f}). Incrementing Retry.")
114
  return {
115
  "market_cap": mkt_cap,
116
  "is_small_cap": False,
117
+ "retry_count": current_retries + 1 # ๐Ÿ‘ˆ CRITICAL FIX
 
118
  }
119
 
120
+ except Exception as e:
121
+ print(f" โŒ YFinance Error: {e}")
122
  return {
123
  "is_small_cap": False,
124
+ "market_cap": 0,
125
+ "retry_count": current_retries + 1 # ๐Ÿ‘ˆ CRITICAL FIX
126
  }
127
 
128
  def analyst_node(state):
129
  """
130
+ ๐Ÿง  THE ANALYST: Runs Graham Logic.
131
  """
132
  ticker = state['ticker']
133
+ print(f" ๐Ÿงฎ Analyzing {ticker}...")
134
+
135
  fin_data = check_financial_health(ticker)
136
+ news = brave_market_search(f"{ticker} stock analysis")
137
 
138
  prompt = f"""
139
+ Analyze {state.get('company_name', ticker)} ({ticker}).
140
+ Market Cap: ${state.get('market_cap', 0):,.0f}
141
+ Financials: {fin_data.get('metrics')}
142
+ News: {news}
143
+ Verdict: BUY / WATCH / AVOID.
144
+ Thesis: 3 sentences max.
 
 
 
 
 
 
 
 
 
145
  """
146
 
147
  if llm:
 
154
 
155
  def email_node(state):
156
  """
157
+ ๐Ÿ“ง THE REPORTER: Sends Success OR Failure reports.
158
  """
 
159
  region = state.get('region', 'Global')
160
+ ticker = state.get('ticker', 'Unknown')
161
  verdict = state.get('final_verdict', 'No Verdict')
162
 
163
+ # ๐Ÿšจ FAILURE REPORT (New Feature)
164
  if not state.get('is_small_cap'):
165
+ print(f" โš ๏ธ Sending Failure Report for {region}...")
166
+ subject = f"๐Ÿงฌ Micro-Cap Hunter: No Targets Found ({region})"
167
+ html_body = f"""
168
+ <h1>โŒ Hunt Failed: {region}</h1>
169
+ <p>Scouted {MAX_RETRIES + 1} times but found no companies meeting the strict Micro-Cap criteria ($10M - $300M).</p>
170
+ <hr>
171
+ <small>Agent: PrimoGreedy</small>
172
+ """
173
+ else:
174
+ # โœ… SUCCESS REPORT
175
+ print(f" ๐Ÿ“จ Sending Analysis for {ticker}...")
176
+ subject = f"๐Ÿงฌ Micro-Cap Found ({region}): {ticker}"
177
+ html_body = f"""
178
+ <h1>๐Ÿ“ Region: {region}</h1>
179
+ <h2>Ticker: {ticker}</h2>
180
+ <h3>Market Cap: ${state.get('market_cap', 0):,.0f}</h3>
181
+ <hr>
182
+ {verdict.replace(chr(10), '<br>')}
183
+ <hr>
184
+ """
185
 
186
  team = [
187
  {"name": "Cisco", "email": os.getenv("EMAIL_CISCO"), "key": os.getenv("RESEND_API_KEY_CISCO")},
 
207
  workflow.set_entry_point("scout")
208
 
209
  def check_status(state):
210
+ # 1. Found a Gem? -> Analyze
211
+ if state.get('is_small_cap'):
212
  return "analyst"
213
 
214
+ # 2. Retries exhausted? -> Email Failure
215
+ if state.get('retry_count', 0) > MAX_RETRIES:
216
+ return "email" # ๐Ÿ‘ˆ NOW GOES TO EMAIL INSTEAD OF END
217
 
218
+ # 3. Try again? -> Loop back
219
+ print(" ๐Ÿ”„ Looping back...")
220
+ time.sleep(2) # Safety Pause
221
+ return "scout"
222
 
223
  workflow.add_edge("scout", "gatekeeper")
224
+ workflow.add_conditional_edges("gatekeeper", check_status,
225
+ {"analyst": "analyst", "scout": "scout", "email": "email"}
226
+ )
227
  workflow.add_edge("analyst", "email")
228
  workflow.add_edge("email", END)
229
 
 
231
 
232
  # ๐ŸŸข EXECUTION BLOCK
233
  if __name__ == "__main__":
234
+ print("๐Ÿš€ Starting Micro-Cap Hunter (Senior Fixed Version)...")
235
  regions = ["USA", "UK", "Canada", "Australia"]
236
 
237
  for market in regions:
238
+ print(f"\n--- ๐Ÿ Hunt: {market} ---")
239
  try:
240
+ # Explicitly reset retry_count to 0
241
  app.invoke({"region": market, "retry_count": 0, "ticker": ""})
242
+ print(f"โœ… {market} Complete.")
 
243
  except Exception as e:
244
+ print(f"โŒ Critical Error in {market}: {e}")