Compare commits
2 Commits
f708f3bf1d
...
f676f44645
Author | SHA1 | Date | |
---|---|---|---|
f676f44645 | |||
e015eef61e |
@@ -64,26 +64,39 @@ class OpenInsiderTopCrawler(BaseCrawler):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_money(val: str) -> Optional[int]:
|
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:
|
if not val:
|
||||||
return None
|
return None
|
||||||
s = val.strip()
|
s = val.strip()
|
||||||
# Remove $ and commas and any parentheses
|
# Detect negative indicators before stripping
|
||||||
for ch in ['$', ',', '(', ')', '+']:
|
is_negative = s.startswith('-') or '(' in s
|
||||||
|
# Normalize: remove currency symbols, commas, parentheses, plus/minus, spaces
|
||||||
|
for ch in ['$', ',', '(', ')', '+', '-', ' ']:
|
||||||
s = s.replace(ch, '')
|
s = s.replace(ch, '')
|
||||||
# Some cells may include text like "$1,234,567 (incl. options)"
|
# Keep only leading digits
|
||||||
# Keep only leading numeric part
|
|
||||||
num = ''
|
num = ''
|
||||||
for c in s:
|
for c in s:
|
||||||
if c.isdigit():
|
if c.isdigit():
|
||||||
num += c
|
num += c
|
||||||
elif c in ' .':
|
elif c == '.':
|
||||||
|
# ignore decimal points; values appear to be whole dollars
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if not num:
|
if not num:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return int(num)
|
value = int(num)
|
||||||
|
# We return absolute magnitude; sign is not needed for threshold
|
||||||
|
return abs(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -148,14 +161,21 @@ class OpenInsiderTopCrawler(BaseCrawler):
|
|||||||
amount = self._parse_money(value_text)
|
amount = self._parse_money(value_text)
|
||||||
if amount is None or amount < self.min_amount:
|
if amount is None or amount < self.min_amount:
|
||||||
continue
|
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}"
|
hash_src = f"{ticker}|{insider}|{trans_type}|{qty}|{price}|{trade_date}|{amount}|{url}"
|
||||||
items.append({
|
items.append({
|
||||||
'title': title,
|
'title': title,
|
||||||
'link': url,
|
'link': url,
|
||||||
'scraped_at': datetime.now().isoformat(),
|
'scraped_at': datetime.now().isoformat(),
|
||||||
'hash': hashlib.md5(hash_src.encode('utf-8')).hexdigest()[:12],
|
'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
|
return items
|
||||||
@@ -221,10 +241,30 @@ class OpenInsiderTopCrawler(BaseCrawler):
|
|||||||
subject = f"OpenInsider 當日大額內部人交易(≥${self.min_amount:,}) - {len(items)} 筆"
|
subject = f"OpenInsider 當日大額內部人交易(≥${self.min_amount:,}) - {len(items)} 筆"
|
||||||
lines = []
|
lines = []
|
||||||
for it in items[:10]:
|
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 = (
|
body = (
|
||||||
f"發現 {len(items)} 筆符合金額門檻的內部人交易(OpenInsider):\n\n" + "\n".join(lines) + "\n\n"
|
f"發現 {len(items)} 筆符合金額門檻的內部人交易(OpenInsider):\n\n" + "\n".join(lines) + "\n\n"
|
||||||
f"抓取時間:{datetime.now().isoformat()}\n來源:\n- " + "\n- ".join(self.urls)
|
f"抓取時間:{datetime.now().isoformat()}\n來源:\n- " + "\n- ".join(self.urls)
|
||||||
)
|
)
|
||||||
return subject, body
|
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