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,123 @@
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
import config
|
||||
|
||||
|
||||
def get_connection():
|
||||
conn = sqlite3.connect(config.DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
return conn
|
||||
|
||||
|
||||
def init_db():
|
||||
schema_path = os.path.join(os.path.dirname(__file__), "schema.sql")
|
||||
with open(schema_path, "r") as f:
|
||||
schema = f.read()
|
||||
conn = get_connection()
|
||||
conn.executescript(schema)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def insert_filing(filing: dict) -> bool:
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO filings
|
||||
(accession_number, ticker, cik, insider_name, role,
|
||||
transaction_date, filed_date, shares, price, total_value,
|
||||
flag, is_10b51, post_tx_shares)
|
||||
VALUES
|
||||
(:accession_number, :ticker, :cik, :insider_name, :role,
|
||||
:transaction_date, :filed_date, :shares, :price, :total_value,
|
||||
:flag, :is_10b51, :post_tx_shares)
|
||||
""",
|
||||
filing,
|
||||
)
|
||||
inserted = conn.execute("SELECT changes()").fetchone()[0] > 0
|
||||
conn.commit()
|
||||
return inserted
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def insert_signal(signal: dict) -> int:
|
||||
conn = get_connection()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO signals
|
||||
(ticker, trigger_date, cluster_size, total_cluster_value, score)
|
||||
VALUES
|
||||
(:ticker, :trigger_date, :cluster_size, :total_cluster_value, :score)
|
||||
""",
|
||||
signal,
|
||||
)
|
||||
signal_id = cur.lastrowid
|
||||
conn.commit()
|
||||
return signal_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def mark_signal_alerted(signal_id: int):
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute("UPDATE signals SET alerted=1 WHERE id=?", (signal_id,))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def mark_signal_executed(signal_id: int):
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute("UPDATE signals SET executed=1 WHERE id=?", (signal_id,))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_unalerted_signals():
|
||||
conn = get_connection()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM signals WHERE alerted=0 ORDER BY created_at ASC"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_recent_buys_for_ticker(ticker: str, window_days: int) -> list:
|
||||
conn = get_connection()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT * FROM filings
|
||||
WHERE ticker=?
|
||||
AND flag='A'
|
||||
AND is_10b51=0
|
||||
AND transaction_date >= date('now', ? || ' days')
|
||||
ORDER BY transaction_date DESC
|
||||
""",
|
||||
(ticker, f"-{window_days}"),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def accession_exists(accession_number: str) -> bool:
|
||||
conn = get_connection()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM filings WHERE accession_number=?", (accession_number,)
|
||||
).fetchone()
|
||||
return row is not None
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1,34 @@
|
||||
CREATE TABLE IF NOT EXISTS filings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
accession_number TEXT UNIQUE,
|
||||
ticker TEXT,
|
||||
cik TEXT,
|
||||
insider_name TEXT,
|
||||
role TEXT,
|
||||
transaction_date TEXT,
|
||||
filed_date TEXT,
|
||||
shares REAL,
|
||||
price REAL,
|
||||
total_value REAL,
|
||||
flag TEXT,
|
||||
is_10b51 INTEGER DEFAULT 0,
|
||||
post_tx_shares REAL,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ticker TEXT,
|
||||
trigger_date TEXT,
|
||||
cluster_size INTEGER,
|
||||
total_cluster_value REAL,
|
||||
score REAL,
|
||||
alerted INTEGER DEFAULT 0,
|
||||
executed INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_filings_ticker ON filings(ticker);
|
||||
CREATE INDEX IF NOT EXISTS idx_filings_transaction_date ON filings(transaction_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_ticker ON signals(ticker);
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_alerted ON signals(alerted);
|
||||
Reference in New Issue
Block a user