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)
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from db.db import get_recent_buys_for_ticker
|
||||
import config
|
||||
|
||||
|
||||
def detect_cluster(ticker: str) -> dict:
|
||||
buys = get_recent_buys_for_ticker(ticker, config.CLUSTER_WINDOW_DAYS)
|
||||
unique_insiders = {b["insider_name"] for b in buys}
|
||||
total_value = sum(b["total_value"] or 0 for b in buys)
|
||||
return {
|
||||
"cluster_size": len(unique_insiders),
|
||||
"total_cluster_value": total_value,
|
||||
"buys": buys,
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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
|
||||
Reference in New Issue
Block a user