smaug/signals/filter_engine.py
Claude 7e9221a914 feat: add PLAN.md and insider copytrade POC implementation
- PLAN.md: full implementation plan from issue
- config.py: configurable thresholds, API keys via .env
- ingestion/: EDGAR RSS poller + Form 4 XML parser
- db/: SQLite schema + interface (WAL mode)
- signals/: filter engine (buy/10b5-1/value/role) + cluster detector
- alerts/: Slack webhook alert with score gating
- broker/: Alpaca paper/live trade execution
- backtest/: historical signal backtesting with yfinance
- main.py: CLI entrypoint (run | fetch-once | backtest)
2026-05-04 16:15:22 +00:00

68 lines
1.9 KiB
Python

import math
import logging
from typing import Optional
import config
from signals.cluster_detector import detect_cluster
from db.db import insert_signal
logger = logging.getLogger(__name__)
def _role_weight(role: str) -> float:
role_lower = (role or "").lower()
for key, weight in config.ROLE_WEIGHTS.items():
if key in role_lower:
return weight
return config.DEFAULT_ROLE_WEIGHT
def _score(total_value: float, role: str, cluster_size: int) -> float:
if not total_value or total_value <= 0:
return 0.0
base = _role_weight(role)
cluster_mult = 1.0 + 0.5 * (cluster_size - 1)
return base * math.log(total_value) * cluster_mult
def process_filing(filing: dict) -> Optional[dict]:
if filing.get("flag") != "A":
return None
if filing.get("is_10b51"):
logger.debug(f"Skipping 10b5-1 filing: {filing['accession_number']}")
return None
total_value = filing.get("total_value") or 0
if total_value < config.MIN_TRANSACTION_VALUE:
logger.debug(f"Below min value: {filing['accession_number']} (${total_value:,.0f})")
return None
ticker = filing.get("ticker", "")
if not ticker:
return None
cluster_info = detect_cluster(ticker)
cluster_size = cluster_info["cluster_size"]
total_cluster_value = cluster_info["total_cluster_value"]
if cluster_size < config.MIN_CLUSTER_SIZE:
return None
score = _score(total_value, filing.get("role", ""), cluster_size)
signal = {
"ticker": ticker,
"trigger_date": filing.get("transaction_date", ""),
"cluster_size": cluster_size,
"total_cluster_value": total_cluster_value,
"score": round(score, 2),
"filing": filing,
"cluster_buys": cluster_info["buys"],
}
signal_id = insert_signal(signal)
signal["id"] = signal_id
logger.info(f"Signal generated: {ticker} score={score:.2f} cluster={cluster_size}")
return signal