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:
|
||||
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(
|
||||
{
|
||||
"ticker": ticker,
|
||||
|
|
@ -101,11 +104,13 @@ class InsiderBuyingScanner(BaseScanner):
|
|||
"insider_title": title,
|
||||
"transaction_value": value,
|
||||
"num_insiders_buying": num_insiders,
|
||||
"insider_score": insider_score,
|
||||
}
|
||||
)
|
||||
|
||||
if len(candidates) >= self.limit:
|
||||
break
|
||||
# Sort by signal quality, then limit
|
||||
candidates.sort(key=lambda c: c.get("insider_score", 0), reverse=True)
|
||||
candidates = candidates[: self.limit]
|
||||
|
||||
logger.info(f"Insider buying: {len(candidates)} candidates")
|
||||
return candidates
|
||||
|
|
|
|||
|
|
@ -123,18 +123,20 @@ class SectorRotationScanner(BaseScanner):
|
|||
continue
|
||||
|
||||
# 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%.
|
||||
candidates = []
|
||||
for ticker, ret_5d in ticker_returns.items():
|
||||
if ret_5d > 2.0:
|
||||
continue # Already moved — not a laggard
|
||||
# Sort by most-negative return first — best laggards checked before limit.
|
||||
laggards = [(t, r) for t, r in ticker_returns.items() if r <= 2.0]
|
||||
laggards.sort(key=lambda x: x[1])
|
||||
|
||||
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
|
||||
|
||||
api_calls += 1
|
||||
result = self._check_sector_laggard(ticker, accelerating_sectors, get_ticker_info)
|
||||
if result:
|
||||
# Overwrite ret_5d with the value we already computed
|
||||
result["stock_5d_return"] = round(ret_5d, 2)
|
||||
candidates.append(result)
|
||||
|
||||
|
|
|
|||
|
|
@ -85,12 +85,12 @@ class TechnicalBreakoutScanner(BaseScanner):
|
|||
result = self._check_breakout(ticker, data)
|
||||
if 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 = candidates[: self.limit]
|
||||
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]]:
|
||||
"""Check if ticker has a volume-confirmed breakout."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue