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,64 @@
|
||||
import logging
|
||||
import requests
|
||||
|
||||
import config
|
||||
from db.db import mark_signal_alerted
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_signal(signal: dict) -> str:
|
||||
filing = signal.get("filing", {})
|
||||
ticker = signal["ticker"]
|
||||
insider = filing.get("insider_name", "Unknown")
|
||||
role = filing.get("role", "Unknown")
|
||||
tx_date = filing.get("transaction_date", "")
|
||||
shares = filing.get("shares")
|
||||
price = filing.get("price")
|
||||
total_value = filing.get("total_value") or signal.get("total_cluster_value", 0)
|
||||
cluster_size = signal["cluster_size"]
|
||||
score = signal["score"]
|
||||
is_10b51 = "Yes" if filing.get("is_10b51") else "No"
|
||||
accession = filing.get("accession_number", "")
|
||||
|
||||
shares_str = f"{shares:,.0f}" if shares else "N/A"
|
||||
price_str = f"${price:,.2f}" if price else "N/A"
|
||||
value_str = f"${total_value:,.0f}" if total_value else "N/A"
|
||||
edgar_url = f"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&type=4&dateb=&owner=include&count=10&search_text=&ticker={ticker}"
|
||||
|
||||
return (
|
||||
f"*INSIDER BUY SIGNAL*\n"
|
||||
f"Ticker: ${ticker}\n"
|
||||
f"Insider: {insider} ({role})\n"
|
||||
f"Date: {tx_date}\n"
|
||||
f"Shares: {shares_str} @ {price_str} = {value_str}\n"
|
||||
f"Cluster: {cluster_size} insider(s) in last {config.CLUSTER_WINDOW_DAYS} days\n"
|
||||
f"Score: {score}\n"
|
||||
f"10b5-1: {is_10b51}\n"
|
||||
f"EDGAR: {edgar_url}"
|
||||
)
|
||||
|
||||
|
||||
def send_slack_alert(signal: dict) -> bool:
|
||||
if not config.SLACK_WEBHOOK_URL:
|
||||
logger.warning("SLACK_WEBHOOK_URL not configured")
|
||||
return False
|
||||
|
||||
if signal.get("score", 0) < config.SCORE_ALERT_THRESHOLD:
|
||||
logger.debug(f"Signal score {signal['score']} below threshold {config.SCORE_ALERT_THRESHOLD}")
|
||||
return False
|
||||
|
||||
text = format_signal(signal)
|
||||
try:
|
||||
resp = requests.post(
|
||||
config.SLACK_WEBHOOK_URL,
|
||||
json={"text": text},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
mark_signal_alerted(signal["id"])
|
||||
logger.info(f"Slack alert sent for {signal['ticker']}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send Slack alert: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user