fix(scanners): rank by signal quality before limiting in 3 more scanners
Same issue as options_flow: early exit on candidate count discards strong signals that happen to be later in iteration order. insider_buying: Dict iteration order matched OpenInsider HTML scrape order, not signal quality. Now scores by cluster buys + C-suite + dollar value, then takes top N. technical_breakout: Stopped at limit*2 in file order despite data already being batch-downloaded (zero API cost to check all). Removed early exit, scan full universe, sort by volume_multiple. sector_rotation: Checked laggards in arbitrary dict order, spending API calls on random tickers. Now sorts by most-negative 5d return first so the strongest laggard candidates are checked before hitting the budget. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
136fa47645
commit
719a2d3f4e
|
|
@ -90,6 +90,9 @@ class InsiderBuyingScanner(BaseScanner):
|
||||||
else:
|
else:
|
||||||
context = f"{title} {insider_name} purchased {value_str} of {ticker}"
|
context = f"{title} {insider_name} purchased {value_str} of {ticker}"
|
||||||
|
|
||||||
|
# Scoring: cluster buys > C-suite > dollar value
|
||||||
|
insider_score = value + (num_insiders * 500_000) + (1_000_000 if is_c_suite else 0)
|
||||||
|
|
||||||
candidates.append(
|
candidates.append(
|
||||||
{
|
{
|
||||||
"ticker": ticker,
|
"ticker": ticker,
|
||||||
|
|
@ -101,11 +104,13 @@ class InsiderBuyingScanner(BaseScanner):
|
||||||
"insider_title": title,
|
"insider_title": title,
|
||||||
"transaction_value": value,
|
"transaction_value": value,
|
||||||
"num_insiders_buying": num_insiders,
|
"num_insiders_buying": num_insiders,
|
||||||
|
"insider_score": insider_score,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(candidates) >= self.limit:
|
# Sort by signal quality, then limit
|
||||||
break
|
candidates.sort(key=lambda c: c.get("insider_score", 0), reverse=True)
|
||||||
|
candidates = candidates[: self.limit]
|
||||||
|
|
||||||
logger.info(f"Insider buying: {len(candidates)} candidates")
|
logger.info(f"Insider buying: {len(candidates)} candidates")
|
||||||
return candidates
|
return candidates
|
||||||
|
|
|
||||||
|
|
@ -123,18 +123,20 @@ class SectorRotationScanner(BaseScanner):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Step 3: Only call get_ticker_info() for laggard tickers (< 2% 5d move).
|
# Step 3: Only call get_ticker_info() for laggard tickers (< 2% 5d move).
|
||||||
# This dramatically reduces API calls from max_tickers down to ~20-30%.
|
# Sort by most-negative return first — best laggards checked before limit.
|
||||||
candidates = []
|
laggards = [(t, r) for t, r in ticker_returns.items() if r <= 2.0]
|
||||||
for ticker, ret_5d in ticker_returns.items():
|
laggards.sort(key=lambda x: x[1])
|
||||||
if ret_5d > 2.0:
|
|
||||||
continue # Already moved — not a laggard
|
|
||||||
|
|
||||||
if len(candidates) >= self.limit:
|
candidates = []
|
||||||
|
api_calls = 0
|
||||||
|
max_api_calls = self.limit * 3 # budget for get_ticker_info calls
|
||||||
|
for ticker, ret_5d in laggards:
|
||||||
|
if len(candidates) >= self.limit or api_calls >= max_api_calls:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
api_calls += 1
|
||||||
result = self._check_sector_laggard(ticker, accelerating_sectors, get_ticker_info)
|
result = self._check_sector_laggard(ticker, accelerating_sectors, get_ticker_info)
|
||||||
if result:
|
if result:
|
||||||
# Overwrite ret_5d with the value we already computed
|
|
||||||
result["stock_5d_return"] = round(ret_5d, 2)
|
result["stock_5d_return"] = round(ret_5d, 2)
|
||||||
candidates.append(result)
|
candidates.append(result)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,12 +85,12 @@ class TechnicalBreakoutScanner(BaseScanner):
|
||||||
result = self._check_breakout(ticker, data)
|
result = self._check_breakout(ticker, data)
|
||||||
if result:
|
if result:
|
||||||
candidates.append(result)
|
candidates.append(result)
|
||||||
if len(candidates) >= self.limit * 2:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
# Sort by strongest breakout signal, then limit
|
||||||
candidates.sort(key=lambda c: c.get("volume_multiple", 0), reverse=True)
|
candidates.sort(key=lambda c: c.get("volume_multiple", 0), reverse=True)
|
||||||
|
candidates = candidates[: self.limit]
|
||||||
logger.info(f"Technical breakouts: {len(candidates)} candidates")
|
logger.info(f"Technical breakouts: {len(candidates)} candidates")
|
||||||
return candidates[: self.limit]
|
return candidates
|
||||||
|
|
||||||
def _check_breakout(self, ticker: str, data: pd.DataFrame) -> Optional[Dict[str, Any]]:
|
def _check_breakout(self, ticker: str, data: pd.DataFrame) -> Optional[Dict[str, Any]]:
|
||||||
"""Check if ticker has a volume-confirmed breakout."""
|
"""Check if ticker has a volume-confirmed breakout."""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue