This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.2.0 instead.
Stock Swing Attribution Agent
Detects unusual price moves, retrieves news across sources, and produces an evidence-backed explanation of why a stock moved — or states that no catalyst can be identified.
Specs: agent-plan.md (what and why) · data-sources.md (APIs) ·
CODEBASE-PLAN.md (module layout, schema, build order).
Using it
swing # opens the prompt
> why is NVDA down?
> what happened to Tesla yesterday?
> why did MU jump on Aug 27?
One-shot forms: swing ask "why is NVDA down?" · swing why NVDA --date 2026-08-27.
- Covers the 12 watchlist stocks: NVDA MRVL MU SNDK AVGO QCOM AAPL GOOGL NFLX TTWO TSLA SBUX.
- Each question fetches fresh prices and news first. A normal day is answered in a second or two with no model call; an unusual move takes ~20s and uses one Gemini request (free tier: 20/day). Asking again reuses the stored answer.
- Evidence: SEC filings, company press releases, Reuters and Bloomberg wire stories, WSJ / CNBC / MarketWatch / Dow Jones, analyst rating changes, and news about related companies (competitors, big customers, suppliers), each placed before or after the moment the move started.
- When nothing published before the move explains it, it says so instead of guessing.
- It refuses forecasts ("is NVDA a buy?").
- Answers are only as good as the news it holds, so keep the collector running (below).
Everything else (annotate, placebo, metrics, daily, ...) is evaluation
and operations tooling.
Runbook
Installing the swing command
Installed globally (editable, so code edits take effect immediately):
uv tool install --editable ".[data,ml,llm]" --python 3.12 --force
swing health # per-source feed health
swing coverage # what data do I have
swing dbinit # apply schema, idempotent
The collector
tail -f data/collector.log # watch it
swing health # per-source counts + broken-feed check
kill $(cat data/collector.pid) # stop
nohup caffeinate -is swing collect --daemon > data/collector.nohup.log 2>&1 &
echo $! > data/collector.pid # start (see the sleep caveat below)
Polls: EDGAR every 10 min (12 stocks + 21 related companies) · IR/press RSS every 10-15 min · Finnhub news every 6 h · analyst ratings hourly · normalize every 5 min · dead-feed check every 30 min.
Scheduled inside the collector (interface/schedule.py), so no cron is needed:
- Weekdays 16:45 ET — refresh, detect the day's swings, explain up to 3 new big moves, alert on |z| ≥ 3.
- Nightly 00:30 PT — 17 placebo cases toward Gate 4's 200 (about 12 nights); the job stops by itself at 200.
That is up to 20 of the 20 free Gemini requests a day: on a day the post-close run
explains moves, unusual-move questions wait until the quota resets at midnight
Pacific. Normal days and repeated questions cost nothing. Each job records its last run in
data/schedule_*.last; delete a file to make that job run again today.
⚠️ Two persistence caveats — read both
1. This Mac sleeps after 1 minute idle (pmset reports sleep 1,
powernap 0 on battery). time.monotonic() freezes across macOS sleep, so the
collector stops polling entirely — observed directly: one poll, then 61 minutes
with zero CPU and no polls. That is silent, permanent news loss.
The caffeinate -is wrapper above is the mitigation and is verified working,
but it keeps the Mac awake and does not survive a lid close. The real fix is
an always-on host — a $5 VPS or a Raspberry Pi running Postgres and the
collector. Every other part of this system is a batch job that can run anywhere;
only the collector must never stop.
2. The collector runs under nohup. It survives closing the terminal,
but not a reboot or logout. A macOS LaunchAgent was tried and does not work
from this location: ~/Desktop is TCC-protected, and a launchd background agent
does not inherit Full Disk Access, so the Python interpreter blocks forever in
_PyConfig_InitPathConfig → open() before any project code runs.
Two durable fixes, either one is a few minutes:
- Move the repo out of
~/Desktop(e.g.~/projects/swing-agent), recreate the venv there, then installscripts/com.swingagent.collector.plist:sed "s|__REPO__|$PWD|g" scripts/com.swingagent.collector.plist \ > ~/Library/LaunchAgents/com.swingagent.collector.plist launchctl load ~/Library/LaunchAgents/com.swingagent.collector.plist
- Grant Full Disk Access to the interpreter in System Settings → Privacy & Security → Full Disk Access, then load the plist.
Until then: after any reboot, restart the collector with the command above.
Check --report shows growth every few days. A silently dead feed for three
weeks is three weeks of unrecoverable data.
Database
docker compose up -d # Postgres 16 + pgvector on :5433
docker compose ps
swing dbinit && .venv/bin/alembic upgrade head # schema + migrations, idempotent
docker exec -e PGPASSWORD=swing swing-db psql -U swing -d swing_agent
Layout
src/swing/
cli.py, commands.py the `swing` command and its subcommands
interface/explain.py one question -> prices, news, split, reason (what `swing` runs)
ingest/ collector, EDGAR, RSS, Finnhub, prices, normalize
analysis/ market/sector split, swing detection, onset, clustering, ranking
agent/ LangGraph attribution: Gemini call + code-level citation/abstention guards
eval/ annotation, placebo test, metrics
store/ schema.sql, models, migrations
config/ watchlist.yaml, sources.yaml, thresholds.yaml, prompts/
Design and findings: CODEBASE-PLAN.md. Package layout is src/swing/ so a
globally installed swing does not put common or agent on the system as
top-level import names.
License
MIT — see LICENSE.
Disclaimer
This is not investment advice. It is a research tool that reports what the news said before a price move. It does not predict prices, recommend trades, or know anything about your situation.
Two limits are worth stating plainly, because they are measured rather than guessed:
- It finds a catalyst for roughly 70% of the moves it is asked about. The
rest come back
unexplained, and the runswing metricsprints is the real number, not a target. unexplainedmeans no catalyst was found in the sources collected, which is not the same as no catalyst existed. Coverage is bounded by which feeds have been running and for how long.
The agent is built to abstain rather than guess — in 38 tests where it was given deliberately unrelated evidence, it invented a cause 0 times — but an explanation it does give is still a starting point for your own reading, not a conclusion. Verify anything you act on against the linked source.
Release files for swing-agent 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| swing_agent-0.1.0.tar.gz | 184.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| swing_agent-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 359.0 kB
Release files / swing_agent-0.1.0.tar.gz
| Download URL | swing_agent-0.1.0.tar.gz |
|---|---|
| Size | 184.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b66ce330ad901910281af858de0562b5c315b9487c17000278ca1de8bf3799f0
|
|
BLAKE2b-256 checksum How to use checksums |
feed4f5386d449cda92d4ae8782c36aff7185fb47508067e1d1027bdd6652ac9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / swing_agent-0.1.0-py3-none-any.whl
| Download URL | swing_agent-0.1.0-py3-none-any.whl |
|---|---|
| Size | 175.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ed1c97c1a2cffcd119223c33c9b676aa3c5659734e24b527c976a4ee1251437c
|
|
BLAKE2b-256 checksum How to use checksums |
c17307a66ed270887664283ed6c76c352e8ec2815c101ddeff1930deae9b43d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log