Compare commits

...

2 Commits

Author SHA1 Message Date
f676f44645 feat(openinsider_top): add trade type tag in notifications and ensure absolute amount display
- Prefix titles and email lines with normalized trade tag (BUY/SALE/...)\n- Include amount as absolute value in lines; keep threshold logic unchanged
2025-09-09 21:32:07 +08:00
e015eef61e fix(openinsider_top): treat negative Value on sales as absolute for threshold parsing
Handle strings like '-,234,567' by normalizing and parsing magnitude; ensures sales rows are included when exceeding INSIDER_MIN_AMOUNT.
2025-09-09 21:26:52 +08:00

View File

@@ -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]