feat(cli): add backfill and simulate commands; historical signal reprocessing
- backfill: bulk-ingest SEC EDGAR quarterly archives (--years / --year --quarter), then regenerate signals with as-of-date awareness - simulate: delegate to backtest/simulate.py with full cost params - _run_signals: deduplicates (ticker, date) pairs, slices dates to 10 chars to avoid strptime crash on timezone-suffixed transaction_date values - Remove fetch-once command (superseded by backfill) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1467033aa2
commit
8f666130b9
83
main.py
83
main.py
@ -8,6 +8,25 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_signals(label: str = ""):
|
||||||
|
from db.db import get_all_buys_for_reprocess
|
||||||
|
from signals.filter_engine import process_filing
|
||||||
|
|
||||||
|
filings = get_all_buys_for_reprocess()
|
||||||
|
count = 0
|
||||||
|
seen: set[tuple] = set()
|
||||||
|
for filing in filings:
|
||||||
|
as_of = (filing.get("transaction_date") or filing.get("filed_date") or "")[:10]
|
||||||
|
key = (filing["ticker"], as_of)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
if process_filing(filing, as_of_date=as_of) is not None:
|
||||||
|
count += 1
|
||||||
|
logger.info(f"Signal generation{' ' + label if label else ''}: {count} signals")
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
def _process_filing(filing: dict):
|
def _process_filing(filing: dict):
|
||||||
from signals.filter_engine import process_filing
|
from signals.filter_engine import process_filing
|
||||||
from alerts.slack_alert import send_slack_alert
|
from alerts.slack_alert import send_slack_alert
|
||||||
@ -17,9 +36,7 @@ def _process_filing(filing: dict):
|
|||||||
if signal is None:
|
if signal is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(
|
logger.info(f"Signal: {signal['ticker']} score={signal['score']} cluster={signal['cluster_size']}")
|
||||||
f"Signal: {signal['ticker']} score={signal['score']} cluster={signal['cluster_size']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if config.SLACK_WEBHOOK_URL:
|
if config.SLACK_WEBHOOK_URL:
|
||||||
send_slack_alert(signal)
|
send_slack_alert(signal)
|
||||||
@ -37,6 +54,7 @@ def _close_expired_positions():
|
|||||||
|
|
||||||
|
|
||||||
def cmd_run():
|
def cmd_run():
|
||||||
|
"""Continuous live polling — polls EDGAR every 10 min, processes signals, executes trades."""
|
||||||
from db.db import init_db
|
from db.db import init_db
|
||||||
from ingestion.edgar_poller import run_poller
|
from ingestion.edgar_poller import run_poller
|
||||||
|
|
||||||
@ -50,7 +68,44 @@ def cmd_run():
|
|||||||
run_poller(on_new_filing=on_new_filing)
|
run_poller(on_new_filing=on_new_filing)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_backfill():
|
||||||
|
"""Bulk-ingest historical Form 4 filings from SEC EDGAR quarterly archives.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python main.py backfill --years 2023 2024 # full year range
|
||||||
|
python main.py backfill --year 2024 --quarter 1 # single quarter
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
from db.db import init_db
|
||||||
|
from ingestion.sec_bulk_ingest import ingest_years, ingest_quarter
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog="main.py backfill")
|
||||||
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("--years", nargs=2, type=int, metavar=("FROM", "TO"),
|
||||||
|
help="Inclusive year range, e.g. --years 2023 2024")
|
||||||
|
group.add_argument("--year", type=int, help="Single year (use with --quarter)")
|
||||||
|
parser.add_argument("--quarter", type=int, choices=[1, 2, 3, 4])
|
||||||
|
parser.add_argument("--no-signals", action="store_true",
|
||||||
|
help="Skip signal generation after ingest")
|
||||||
|
args = parser.parse_args(sys.argv[2:])
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
if args.years:
|
||||||
|
stored = ingest_years(args.years[0], args.years[1])
|
||||||
|
else:
|
||||||
|
if not args.quarter:
|
||||||
|
parser.error("--year requires --quarter")
|
||||||
|
stored = ingest_quarter(args.year, args.quarter)
|
||||||
|
|
||||||
|
logger.info(f"Ingest complete: {stored} transaction rows stored")
|
||||||
|
|
||||||
|
if not args.no_signals:
|
||||||
|
_run_signals("after backfill")
|
||||||
|
|
||||||
|
|
||||||
def cmd_backtest():
|
def cmd_backtest():
|
||||||
|
"""Backtest signals in the DB against historical prices via yfinance."""
|
||||||
from backtest.backtest import run_backtest, print_summary
|
from backtest.backtest import run_backtest, print_summary
|
||||||
import config
|
import config
|
||||||
|
|
||||||
@ -64,22 +119,24 @@ def cmd_backtest():
|
|||||||
print_summary(summary)
|
print_summary(summary)
|
||||||
|
|
||||||
|
|
||||||
def cmd_fetch_once():
|
def cmd_simulate():
|
||||||
from db.db import init_db
|
"""Portfolio simulation with configurable strategy and transaction cost params.
|
||||||
from ingestion.edgar_poller import fetch_and_store_new_filings
|
|
||||||
|
|
||||||
init_db()
|
Usage:
|
||||||
filings = fetch_and_store_new_filings()
|
python main.py simulate [--holding-days 7] [--buy-delay 1]
|
||||||
logger.info(f"Fetched and stored {len(filings)} new filings")
|
[--position-size 0.10] [--min-score 0] [--min-cluster 1]
|
||||||
|
[--capital 100000]
|
||||||
for filing in filings:
|
[--spread 0.003] [--slippage 0.002] [--commission 0.001]
|
||||||
_process_filing(filing)
|
"""
|
||||||
|
from backtest.simulate import main as sim_main
|
||||||
|
sim_main()
|
||||||
|
|
||||||
|
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
"run": cmd_run,
|
"run": cmd_run,
|
||||||
|
"backfill": cmd_backfill,
|
||||||
"backtest": cmd_backtest,
|
"backtest": cmd_backtest,
|
||||||
"fetch-once": cmd_fetch_once,
|
"simulate": cmd_simulate,
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user