Compare commits
2 Commits
f708f3bf1d
...
f676f44645
Author | SHA1 | Date | |
---|---|---|---|
f676f44645 | |||
e015eef61e |
@@ -64,26 +64,39 @@ class OpenInsiderTopCrawler(BaseCrawler):
|
||||
|
||||
@staticmethod
|
||||
def _parse_money(val: str) -> Optional[int]:
|
||||
"""Parse money text into absolute integer dollars.
|
||||
|
||||
Handles formats like:
|
||||
- "$1,234,567"
|
||||
- "($1,234,567)" (treat as negative but return magnitude)
|
||||
- "-$1,234,567" (treat as negative but return magnitude)
|
||||
- "1,234,567"
|
||||
Returns None if no digits found.
|
||||
"""
|
||||
if not val:
|
||||
return None
|
||||
s = val.strip()
|
||||
# Remove $ and commas and any parentheses
|
||||
for ch in ['$', ',', '(', ')', '+']:
|
||||
# Detect negative indicators before stripping
|
||||
is_negative = s.startswith('-') or '(' in s
|
||||
# Normalize: remove currency symbols, commas, parentheses, plus/minus, spaces
|
||||
for ch in ['$', ',', '(', ')', '+', '-', ' ']:
|
||||
s = s.replace(ch, '')
|
||||
# Some cells may include text like "$1,234,567 (incl. options)"
|
||||
# Keep only leading numeric part
|
||||
# Keep only leading digits
|
||||
num = ''
|
||||
for c in s:
|
||||
if c.isdigit():
|
||||
num += c
|
||||
elif c in ' .':
|
||||
elif c == '.':
|
||||
# ignore decimal points; values appear to be whole dollars
|
||||
continue
|
||||
else:
|
||||
break
|
||||
if not num:
|
||||
return None
|
||||
try:
|
||||
return int(num)
|
||||
value = int(num)
|
||||
# We return absolute magnitude; sign is not needed for threshold
|
||||
return abs(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@@ -148,14 +161,21 @@ class OpenInsiderTopCrawler(BaseCrawler):
|
||||
amount = self._parse_money(value_text)
|
||||
if amount is None or amount < self.min_amount:
|
||||
continue
|
||||
# Normalize trade type tag for notification clarity
|
||||
tag = self._trade_tag(trans_type)
|
||||
|
||||
title = f"{ticker} {trans_type} - {insider} qty {qty} @ {price} value ${amount:,} on {trade_date}"
|
||||
title = f"[{tag}] {ticker} - {insider} qty {qty} @ {price} value ${amount:,} on {trade_date}"
|
||||
hash_src = f"{ticker}|{insider}|{trans_type}|{qty}|{price}|{trade_date}|{amount}|{url}"
|
||||
items.append({
|
||||
'title': title,
|
||||
'link': url,
|
||||
'scraped_at': datetime.now().isoformat(),
|
||||
'hash': hashlib.md5(hash_src.encode('utf-8')).hexdigest()[:12],
|
||||
'trade_type': trans_type,
|
||||
'trade_tag': tag,
|
||||
'ticker': ticker,
|
||||
'amount': amount,
|
||||
'trade_date': trade_date,
|
||||
})
|
||||
|
||||
return items
|
||||
@@ -221,10 +241,30 @@ class OpenInsiderTopCrawler(BaseCrawler):
|
||||
subject = f"OpenInsider 當日大額內部人交易(≥${self.min_amount:,}) - {len(items)} 筆"
|
||||
lines = []
|
||||
for it in items[:10]:
|
||||
lines.append(f"• {it.get('title','')}")
|
||||
tag = it.get('trade_tag') or 'TRADE'
|
||||
ticker = it.get('ticker') or ''
|
||||
amount = it.get('amount')
|
||||
tdate = it.get('trade_date') or ''
|
||||
if isinstance(amount, int):
|
||||
line = f"• [{tag}] {ticker} ${amount:,} on {tdate}"
|
||||
else:
|
||||
line = f"• [{tag}] {it.get('title','')}"
|
||||
lines.append(line)
|
||||
body = (
|
||||
f"發現 {len(items)} 筆符合金額門檻的內部人交易(OpenInsider):\n\n" + "\n".join(lines) + "\n\n"
|
||||
f"抓取時間:{datetime.now().isoformat()}\n來源:\n- " + "\n- ".join(self.urls)
|
||||
)
|
||||
return subject, body
|
||||
|
||||
@staticmethod
|
||||
def _trade_tag(trans_type: str) -> str:
|
||||
t = (trans_type or '').lower()
|
||||
if 'sale' in t or 'sell' in t:
|
||||
return 'SALE'
|
||||
if 'purchase' in t or 'buy' in t:
|
||||
return 'BUY'
|
||||
if 'option' in t:
|
||||
return 'OPTION'
|
||||
if 'gift' in t:
|
||||
return 'GIFT'
|
||||
return (trans_type or 'TRADE').upper()[:20]
|
||||
|
Reference in New Issue
Block a user