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__)
|
||||
|
||||
|
||||
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):
|
||||
from signals.filter_engine import process_filing
|
||||
from alerts.slack_alert import send_slack_alert
|
||||
@ -17,9 +36,7 @@ def _process_filing(filing: dict):
|
||||
if signal is None:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"Signal: {signal['ticker']} score={signal['score']} cluster={signal['cluster_size']}"
|
||||
)
|
||||
logger.info(f"Signal: {signal['ticker']} score={signal['score']} cluster={signal['cluster_size']}")
|
||||
|
||||
if config.SLACK_WEBHOOK_URL:
|
||||
send_slack_alert(signal)
|
||||
@ -37,6 +54,7 @@ def _close_expired_positions():
|
||||
|
||||
|
||||
def cmd_run():
|
||||
"""Continuous live polling — polls EDGAR every 10 min, processes signals, executes trades."""
|
||||
from db.db import init_db
|
||||
from ingestion.edgar_poller import run_poller
|
||||
|
||||
@ -50,7 +68,44 @@ def cmd_run():
|
||||
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():
|
||||
"""Backtest signals in the DB against historical prices via yfinance."""
|
||||
from backtest.backtest import run_backtest, print_summary
|
||||
import config
|
||||
|
||||
@ -64,22 +119,24 @@ def cmd_backtest():
|
||||
print_summary(summary)
|
||||
|
||||
|
||||
def cmd_fetch_once():
|
||||
from db.db import init_db
|
||||
from ingestion.edgar_poller import fetch_and_store_new_filings
|
||||
def cmd_simulate():
|
||||
"""Portfolio simulation with configurable strategy and transaction cost params.
|
||||
|
||||
init_db()
|
||||
filings = fetch_and_store_new_filings()
|
||||
logger.info(f"Fetched and stored {len(filings)} new filings")
|
||||
|
||||
for filing in filings:
|
||||
_process_filing(filing)
|
||||
Usage:
|
||||
python main.py simulate [--holding-days 7] [--buy-delay 1]
|
||||
[--position-size 0.10] [--min-score 0] [--min-cluster 1]
|
||||
[--capital 100000]
|
||||
[--spread 0.003] [--slippage 0.002] [--commission 0.001]
|
||||
"""
|
||||
from backtest.simulate import main as sim_main
|
||||
sim_main()
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
"run": cmd_run,
|
||||
"backfill": cmd_backfill,
|
||||
"backtest": cmd_backtest,
|
||||
"fetch-once": cmd_fetch_once,
|
||||
"simulate": cmd_simulate,
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user