Skip to main content

swing

Why did that stock move? swing detects unusual price moves, finds the news published before they started, and explains them — or tells you it can't.

$ swing why NVDA --date 2025-10-06

NVDA fell 1.1% on Mon Oct 6.
  Split: market +0.6% · chip stocks +1.3% · NVDA on its own -3.1%

Why: AMD announced a major strategic partnership with OpenAI to deploy
6 gigawatts of GPUs, presenting a substantial competitive threat to
Nvidia's AI chip market dominance.
  • Mon Oct 6, 7:04am ET · SEC filing · AMD 8-K — Item 1.01,3.02,7.01,9.01
    — AMD AND OPENAI ANNOUNCE STRATEGIC PARTNERSHIP TO DEPLOY 6 GIGAWATTS
    https://www.sec.gov/Archives/edgar/data/2488/000119312525230895/d28189d8k.htm
  • Mon Oct 6, 7:30am ET · CNBC · OpenAI looks to take 10% stake in AMD
    https://www.cnbc.com/2025/10/06/openai-amd-chip-deal-ai.html
  • Mon Oct 6, 8:15am ET · Theverge · AMD teams up with OpenAI to challenge
    Nvidia's AI chip dominance
    https://www.theverge.com/news/792650/amd-openai-five-year-ai-chip-agreement
  Confidence: high

Note the split. NVDA closed down only 1.1%, which on its own looks like noise — but the market was up and chip stocks were up 1.3%, so the company's own move was -3.1%. That is the move worth explaining, and the explanation came from a competitor's SEC filing, timestamped 2h26m before the drop began.


The one thing to know

swing abstains. When nothing published before a move explains it, it says unexplained instead of assembling a plausible story from whatever is nearby.

In 38 tests where it was handed deliberately unrelated evidence, it invented a cause 0 times. That is the property the rest of the design exists to protect, and it is measured on every release rather than asserted.


How it works

  ┌─ collect ──────────────────────────────────────────────────────────┐
  │  SEC EDGAR · GDELT (Reuters/Bloomberg/WSJ) · company IR feeds ·    │
  │  CNBC · MarketWatch · analyst ratings · press wires                │
  └────────────────────────────┬───────────────────────────────────────┘
                               ▼
  1. DECOMPOSE   Split the day's return into market + sector + the part
                 that is this company alone. A stock up 3% on a day the
                 market is up 3% has not done anything.

  2. DETECT      Flag only moves where the idiosyncratic residual is
                 statistically unusual for that ticker.

  3. TIME        Find the minute the move actually started, from intraday
                 bars. This is what makes "before" mean something.

  4. RETRIEVE    Cluster news around that moment and rank it. Everything
                 is labelled pre_move or post_move against the onset.

  5. EXPLAIN     Ask the model to pick a catalyst from the pre-move
                 evidence, citing specific articles — or to abstain.

  6. GUARD       Drop any citation that does not resolve to a real
                 retrieved article, then re-check the answer still stands.
                 A verdict resting on a dropped citation becomes
                 `unexplained`.

Steps 1-4 use no model at all. The model only writes the final judgement, and it can only cite what step 4 actually retrieved.


Install

Needs Python 3.12.

pip install swing-agent        # or: uv tool install swing-agent
swing init                     # asks for your keys, sets up the database

The install is about 1 GB, mostly PyTorch for the embedding model that ranks news — more on Linux, where PyTorch's default build bundles GPU libraries. That model runs on your machine. Gemini only ever sees the price move and the headlines (plus a summary of up to 300 characters each) of the news it is asked to judge.

swing init walks you through setup and writes everything to ~/.swing/.env.

You need Postgres with pgvector. If you don't have one:

docker run -d --name swing-db -p 5433:5432 \
  -e POSTGRES_USER=swing -e POSTGRES_PASSWORD=swing -e POSTGRES_DB=swing_agent \
  pgvector/pgvector:pg16

Then load history and start collecting:

swing backfill                 # price bars
swing backfill-events          # SEC filings + world news, years back
swing collect --daemon &       # keep running — see below
swing why NVDA

Your Gemini API key

swing works with Google Gemini only. OpenAI, Anthropic and other providers' keys will not work. Every number in this README was measured on gemini-3.6-flash, and the free tier is enough to use it daily.

  1. Get a free key at aistudio.google.com/apikey — sign in with a Google account and click Create API key. It starts with AIza.

  2. Give it to swing:

    swing key                      # paste when asked; typing is hidden
    

That's it. The key is checked with Google before it is saved, so a typo is caught immediately, and checking it does not use any of your daily requests.

swing key --check                  # is my key still working?
swing key                          # replace it

The free tier allows 20 explanations a day per Google Cloud project — not per key, so a second key from the same project adds nothing. Only moves that are actually unusual use a request; quiet days, charts and everything else are free. When the day's 20 are gone, swing says so and resets at midnight Pacific.

If you already export GEMINI_API_KEY in your shell, that copy takes priority over the saved one — swing key will warn you if they differ.

Other keys

Key Needed for Free?
SEC_USER_AGENT SEC filings. Any string with your email in it. yes
DATABASE_URL everything yes
FINNHUB_API_KEY company news, analyst ratings optional, free tier
GOOGLE_CLOUD_PROJECT GDELT wire coverage via BigQuery optional, 1 TB/month free

swing init asks for the first two. Edit ~/.swing/.env for the rest.

⚠️ Keep the collector running

News cannot be backfilled. An RSS feed serves the last 20-50 items and free API tiers cap how far back you can ask, so every hour the collector is not running is coverage that is gone permanently. swing backfill-events recovers SEC filings and GDELT because those two have real archives; nothing else does.

If your machine sleeps, the collector stops. Run it somewhere that stays awake.


Commands

Asking things

swing                          # interactive prompt
swing why NVDA                 # latest session
swing why TSLA --date 2026-09-08
swing ask "what is unexplained recently"
swing unexplained              # moves it could not account for
swing stats NVDA --days 90     # beta, R², residual vol, swing frequency
swing compare NVDA AVGO MRVL   # idiosyncratic share side by side

Running it

swing init                     # first-time setup
swing key                      # add or replace your Gemini API key
swing collect --daemon         # the news collector
swing health                   # per-source freshness, broken feeds
swing monitor                  # operational dashboard
swing daily                    # post-close batch + alerts
swing metrics                  # the evaluation numbers below

Everything else (annotate, placebo, retrieve, factors, …) is pipeline internals and evaluation tooling.


What it is honest about

swing metrics prints these against live data. They are measurements, not targets:

Metric Meaning
confabulation Given unrelated evidence, how often does it invent a cause? Must be near zero.
catalyst coverage Of moves with a knowable cause, how often is the news actually in the corpus? Bounded by your sources, not the model.
attribution accuracy When it does explain, is the explanation right?
abstention precision When it says unexplained, was there really nothing?
citation validity Does every cited article exist? Must be exactly 1.00.

Coverage is the honest weak spot. It runs around 0.70 on a well-fed install — roughly three in ten moves that had a findable cause still come back unexplained, because the story was never collected. More sources and a longer running collector move that number; nothing else does.


Configuration

swing init writes to ~/.swing/:

~/.swing/
  .env                 keys (0600)
  config/
    watchlist.yaml     which tickers, their CIKs and name aliases
    sources.yaml       feeds, their tiers and poll intervals
    thresholds.yaml    what counts as an unusual move
    prompts/           the attribution prompts, versioned
  data/                logs and local state

Edit them freely — swing init never overwrites your changes. Set SWING_HOME to keep them somewhere else.


Development

git clone https://github.com/spoigai21/swing-agent && cd swing-agent
uv sync --extra dev
docker compose up -d
uv run pytest -q

A checkout uses the repo's own config/ directory, so it never touches ~/.swing. Design notes live in agent-plan.md, data-sources.md and CODEBASE-PLAN.md.


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.

unexplained means no catalyst was found in the sources collected, which is not the same as no catalyst existed. An explanation it does give is 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.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for swing-agent 0.2.0
File Size Uploaded
swing_agent-0.2.0.tar.gz 247.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for swing-agent 0.2.0
File Interpreter ABI Platform
swing_agent-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 496.3 kB

Release files / swing_agent-0.2.0.tar.gz

Download URL swing_agent-0.2.0.tar.gz
Size 247.3 kB
Tags Source
SHA-256 checksum
How to use checksums
50dcb12df113ef8251d2b101e0ac7b9385e8d22bb122482d85243fdad5d06fa0
BLAKE2b-256 checksum
How to use checksums
4795bb1f30bc2a27f2a06a48225f94996c122c01a9ab48f73b9f9ac8a730fa74
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

Release files / swing_agent-0.2.0-py3-none-any.whl

Download URL swing_agent-0.2.0-py3-none-any.whl
Size 249.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b205378a7b4b6419464b251d4e6478276d47e6f68b7ee9b430ba804454ead537
BLAKE2b-256 checksum
How to use checksums
81f11c48ad148058b7832d9bc2049aa0ac885b7192a9b1ad1f59cd74591e8cfb
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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page