swarm-trader: The Free Data Is Real, the Risk Layer Is Empty

Twenty agents analyze US stocks on nothing but SEC EDGAR and yfinance. The free-data claim held up under measurement. The code-enforced risk layer did not survi

Part 33. swarm-trader — The Data Really Is Free. The Safety Net Really Is Empty.

You're done with this article when you can run twenty agents against US equities without paying for a single data feed, and you can see for yourself exactly what gets checked — and what doesn't — on the way to the broker.

What it does Thirteen investor personalities plus seven quant specialists each analyze a US stock. A portfolio manager folds their signals into one action — buy, sell, short, cover or hold — and a share count. Orders go to Alpaca paper
Good fit Anyone who wants to run multi-agent analysis without a paid data subscription, or who wants to watch several LLM opinions get merged into one
Bad fit Anyone trading Korean stocks or crypto (there's no path for either) · anyone planning to trust the safety rails (§4.3 is the heart of this article)
What you'll end up with Twenty-one verdicts on twenty-one tickers, one of them filled for real on Alpaca paper, and the rejection I got trying to carry a verdict into weltiq
Cost and prerequisites Code is free (no license file — §1) · data is entirely free (measured, §4.1) · you pay only for LLM tokens — $0.003984 measured for a three-ticker run
Time and difficulty Dependency install 36.083 seconds · first verdict 1 minute 44.60 seconds (three tickers) · difficulty medium
Relationship to weltiq It didn't connect today. The tool called all twenty-one tickers "not a buy," and the shorts among them have no column in weltiq's US equity venue. §5 is that record
Test environment macOS · Python 3.11.16 · commit 3470b82 · 2026-09-24 · LLM fixed at openai/gpt-5.6-luna via OpenRouter
Disclosure I write under a partnership with weltiq. Every command and number here was actually run in the environment above, and every order was simulated. The SHORT calls, share counts and confidence figures are the tool's output, not my opinion

Raw logs and source data: reports/a/33-swarm-trader/


1. What is it

zhound420/swarm-trader puts twenty agents on a single US stock, then has a portfolio manager fold their opinions into an order. It's a fork of virattt/ai-hedge-fund (Part 2 of this series), and the two biggest changes from upstream are swapping paid data for free data and bolting on Alpaca execution.

Value How I checked
Address github.com/zhound420/swarm-trader
License None (license: null) GitHub API · no LICENSE* file
Stars ★58 · 13 forks (observed 2026-09-24) GitHub API
Created 2026-03-08 GitHub API
Last push 2026-03-22 — six months ago GitHub API
Commit I ran 3470b8291c438dc36b22ed37b7e56ba2bbf249b5 git log -1
Code size src/ + app/ 20,135 lines wc -l
Agents 20 (13 LLM personalities + 7 quant) README · --analysts
Broker Alpaca, paper hardcoded (paper-api.alpaca.markets) src/alpaca_integration.py:23

Start with the license, or rather the absence of one. There's no LICENSE file and the GitHub API returns null. The default in that situation is "all rights reserved," so this article quotes code and doesn't reuse it. If you're following along, go in knowing that.

The shape of the thing

trading_mode.json      →  swing or day
    ↓
20 analysts (each: signal + confidence + reasoning)
    ↓
Portfolio Manager      →  action · quantity · confidence · reasoning  ← the LLM decides
    ↓
risk_manager.py        →  the "code-enforced" checks
    ↓
execute_trades.py      →  Alpaca order
    ↓
portfolio_monitor.py   →  stop-loss watch

The README makes two strong claims about that diagram. This article measures both.

"Trades execute on Alpaca paper trading through a code-enforced risk management layer that no agent can override."

"execute_trades.py → Alpaca bracket orders with mandatory stops"


2. Installing

Python 3.11 or newer. I ran 3.11.16.

git clone https://github.com/zhound420/swarm-trader.git
cd swarm-trader
python3.11 -m venv .venv
.venv/bin/pip install -e .

Dependency install took 36.083 seconds. One warning shows up and you can ignore it — WARNING: fastapi 0.104.1 does not provide the extra 'standard'. It only affects the web app.

Fill in .env like this.

ALPACA_API_KEY=...            # Alpaca paper key (free)
ALPACA_API_SECRET=...
OPENROUTER_API_KEY=...        # any one LLM provider. With Ollama you need zero keys
LLM_PROVIDER=OpenRouter
LLM_MODEL=openai/gpt-5.6-luna

There is no dashboard. There's a FastAPI backend under app/, but the real body of this tool is the CLI, and the operations doc (PLAYBOOK.md) explains everything in command lines. Every screen in this article is terminal output.

2.1 The first wall — the "optional" account that blocks the import

Here's what the README says about the day-trading account.

"Day trading account (optional — falls back to primary if not set)" "If only one account is configured, both modes share it (backward compatible)."

That isn't true. Put in only the swing key, run in swing mode, and it dies at import.

Running in swing mode, blocked for want of a day account. src/alpaca_integration.py:33 calls _get_headers("day") at module-load time, so every mode goes through it. The fallback only runs in one direction, swing→day (src/accounts.py:95-100) — there is no day→swing path

The fix is one line. Put the same key in .env a second time.

ALPACA_DAY_API_KEY=<same key as above>
ALPACA_DAY_API_SECRET=<same secret as above>

You need those two lines even if you never intend to run two accounts. I've put it first because following the README exactly will stop you on your first command.

2.2 A latent bug — swing mode sends empty headers for intraday data

In swing mode, gather_data.py reads ALPACA_SWING_API_KEY / ALPACA_SWING_API_SECRET. Those names appear nowhere else in the repository, and not in .env.example either. I called it directly.

swing headers empty? {'APCA-API-KEY-ID': True, 'APCA-API-SECRET-KEY': True}
swing-mode portfolio call -> 401 {"message": "unauthorized."}

The string ALPACA_SWING occurs in exactly three places in the repository: those two lines, and an error message. Run gather_data.py in swing mode and the intraday data never arrives. Adding keys under that name in .env avoids it.


3. Let's just run it once

3.1 The first verdict

The entry point is run_hedge_fund.py. Its own docstring calls it the "Canonical execution script." There's a near-duplicate called run_analysis.py sitting next to it that behaves differently, so use this one (more in §6).

Here's how I asked for it

Analyze NVIDIA, Apple and SPY in swing mode. Don't place any orders yet.

And here's what comes back

Seven analysts attach to each ticker, and per-ticker verdicts land at the end. Look at the three panels near the bottom — NVDA, AAPL and SPY are **all SHORT**, with confidence 82, 84 and 72. And just above them, in the execute_trades log: «APPROVED: SHORT 68 NVDA @ $0.00 ($0 = 0.0% of equity)». That $0.00 is the subject of §4.3

If you'd rather type it yourself
.venv/bin/python run_hedge_fund.py --mode swing --tickers NVDA,AAPL,SPY
Without `--execute` it's a **dry run**. The safe default is the right default here, and it's good design. `--tickers` is **mandatory** when the account holds no positions — otherwise it exits 1.

A three-ticker run took 1 minute 44.60 seconds. Six tickers took 2 minutes 29.39 seconds.

3.2 Five places beginners get stuck

Symptom Cause Fix
ValueError: No Alpaca account configured for mode 'day' The day account is resolved at import time Add ALPACA_DAY_* to .env (§2.1)
No tickers to analyze With no positions, --tickers is required --tickers NVDA,AAPL
Quote not found for symbol: BTCUSD Crypto is not this tool's territory US equities only (S1 in §4)
Swing mode, but intraday data is empty ALPACA_SWING_API_KEY undefined (§2.2) Add keys under that name too
Snapshot for ... already exists It only records once per day. This is normal --force to overwrite

4. Getting serious — the scenarios

4.1 Scenario D. I counted whether the data is actually free

This is the README's proudest claim.

"Zero paid data APIs required." Hybrid data layer uses SEC EDGAR + yfinance. Works fully free out of the box.

Rather than read it and believe it, I intercepted every outbound request and counted the hosts. I called the six data functions directly with hooks on all three transports — requests, urllib, and curl_cffi, which is what yfinance 1.7.0 actually uses.

All six data functions succeeded (80 price bars, 4 financial metrics, market cap, 10 insider trades, 10 news items, 4 line items), and everything that went out went to sec.gov (21 calls) or Yahoo Finance (11 calls). Look at with_auth_header at the bottom — **not one request carried an authentication header.** It all works with no key at all

A full run, all twenty analysts included, looked the same: www.sec.gov 129 calls, data.sec.gov 16 calls, paper-api.alpaca.markets 2 calls. The only authenticated requests were the two to Alpaca, and that key is free too.

Three-line summary

  1. What came out — zero paid hosts. api.financialdatasets.ai ($200/month) is still in the tree, sitting in src/tools/api_original.py with nothing importing it. The live src/tools/api.py is a nine-line file that re-exports api_free
  2. How many steps — six function calls. Zero keys
  3. What doesn't work — the README tells one lie right here. Line 666 describes this layer as a "paid→free fallback hybrid dispatcher." There is no paid path in the code. It wasn't a fallback, it was a wholesale replacement — which, for the user, is the better outcome

On money. The only thing this tool costs to run is LLM tokens. I snapshotted OpenRouter usage before and after a run: $0.003984 for a three-ticker run (openai/gpt-5.6-luna, seven analysts). The data subscription really is zero.

4.2 Scenario D-2 (= S4). Twenty-one tickers, zero buys

What this tool does best is fold several agents' opinions into one, so that's what I measured. Five baskets with genuinely different characters, twenty-one tickers.

I fixed N and the baskets before I started. Picking baskets after seeing results is the moment a table like this turns into advertising.

Run Basket Tickers Result
1 Large-cap tech and index NVDA · AAPL · SPY SHORT 3
2 Safe haven and dividend GLD · TLT · KO SHORT 3
3 Financials, staples, payments MSFT · JPM · JNJ · PG · V · COIN SHORT 4 · HOLD 2
4 Semis and platforms AVGO · TSM · GOOGL · META · AMZN · AMD SHORT 4 · HOLD 2
5 Growth PLTR · CRM · NET SHORT 3
21 tickers SHORT 17 · HOLD 4 · zero buys

Not one buy across twenty-one tickers. It shorted gold. It shorted Treasuries. It shorted Coca-Cola. Confidence ranged widely, from 58% to 94%, with the highest being COIN at 94% — "Near-unanimous bearish signals support a full-size short."

Three-line summary

  1. What came out — twenty-one verdicts. Seventeen short, four hold. A reasoning sentence attached every time
  2. How many steps — one command per run. Three tickers in 1 minute 45 seconds, six in 2 minutes 30 seconds
  3. What doesn't work — this article cannot tell you whether that's a property of the tool or a property of that day's market. I ran them all in one sitting on one day (SPY −0.77%), so there's only one regime in the sample. It needs re-measuring on another day. That said, getting the same direction on gold, Treasuries and consumer staples is worth writing down

This result rewrote §5 entirely — because short has no column in weltiq's US equity venue.

4.3 Scenario S5. I placed one order — and three safety layers turned out to be empty

This section is the heart of the article.

One line in the §3 log kept catching my eye.

APPROVED: SHORT 68 NVDA @ $0.00 ($0 = 0.0% of equity)

Sixty-eight shares of NVDA are not worth zero dollars. So I actually placed an order — exactly the verdict the tool produced in run 4, a 75-share AMZN short — through the tool's own command channel, the same way the README's own cron message tells you to.

echo '{"trades":[{"ticker":"AMZN","action":"short","qty":75,
  "reasoning":"High-confidence bearish signal with no meaningful bullish analyst support."}]}' \
  | .venv/bin/python execute_trades.py --mode swing

The "code-enforced" risk layer waved a 75-share AMZN short through as «$0.00 = 0.0% of equity». Below that is the actual fill — 75 shares at 249.25, market value **−$18,691.5**, which is **18.9%** of the $98,911.92 account. The swing-mode single-position cap is **8%**. And note order_class is an empty string, legs is null, and the open-order list is [] — **there is no protective stop at all**. At the bottom is the code that makes it zero

Why this happens. The verdict crosses a process boundary, and the price falls off on the way.

The risk agent knows the current price perfectly well inside the graph. The portfolio manager uses that price to compute maximum share counts. But the final artifact, PortfolioDecision, has no price field — just action, quantity, confidence and reasoning, with three optional level fields the LLM fills in only if it feels like it.

run_hedge_fund.py serializes that dict to JSON and hands it to execute_trades.py as a separate process. The receiving side doesn't know the price, so it goes looking in the held positions.

# execute_trades.py:392-394
pos = positions.get(ticker, {})                 # empty dict for a new ticker
ref_price = entry_price or limit_price or float(pos.get("current_price", 0))

A ticker you're buying for the first time isn't in the holdings list. So ref_price = 0, and that zero spreads into four places.

What What happens to it Where
8% single-position cap trade_value = qty × 0 = 0 → 0% > 8% is never true → always passes risk_manager.py:176, 351-356
30% sector cap Uses the same trade_value → always passes risk_manager.py:365-374
20% minimum cash Same → always passes risk_manager.py:338-348
Mandatory bracket (−7% stop / 3R target) Only computed when ref > 0 → never attached execute_trades.py:416-430 · :167-181

In other words, these checks only work when you're adding to a position you already hold. On an order that opens a new position — the exact case they exist for — all of them pass in silence.

And the monitor can't see that position

The README says portfolio_monitor.py enforces stops automatically. I ran it while holding the 75-share AMZN short.

It prints «Positions: 1» and puts a single zero-share BTCUSD row in the table. **The 75-share AMZN short is not in the list.** And then: «All positions within stop thresholds» — said to an account carrying an 18.9% unhedged short. Below is why: long_positions filters on qty > 0 and only that list goes into the stop checks. A short has a negative quantity, so it never enters the loop

The README's "Short selling — Full short selling support" and "portfolio_monitor.py → auto-sells stops" are both true and don't mesh with each other. You can open a short, and nobody is watching its stop.

Three-line summary

  1. What came out — one real fill (75 AMZN short at 249.25), and three safety layers empty in a row
  2. How many steps — one command, exactly as the tool's own documentation instructs
  3. What doesn't work — all of the above. But the dry-run default is genuine: without --execute nothing reaches the broker, and the endpoint is hardcoded to paper with no live URL anywhere in the code

4.4 Scenario S7. A stop condition did fire — on the way out

I sent the tool's cover command to clear the position. And got the strangest scene in this article.

Legacy validator BLOCKED cover AMZN:
  Trade value $18,716 exceeds max $7,911 (8% of portfolio)

The very same 8% rule that let me in blocked the way out.

The reason is the picture from §4.3, unchanged. On the way in, AMZN wasn't in the holdings list, so the price was zero. On the way out the position exists, so there's a price. The same arithmetic now sees $18,716 precisely and blocks it for exceeding the $7,911 cap.

The check is blind on entry and sighted only on exit. Which produces this — the tool will not let you out of a size it let you into.

I closed it through the Alpaca API directly, going around the tool.

Entry 75 shares short @ 249.25 · 2026-09-23T19:43:36.523944264Z
Exit 75 shares cover @ 249.450667 · 2026-09-23T19:44:50.545625032Z
Holding time 74 seconds
Realized P&L −15.050025 USD
Equity 98,911.92 → 98,894.90
Exit route Outside the tool. A direct DELETE /v2/positions/AMZN

Three-line summary

  1. What came out — the only case where a stop condition actually fired. And it fired at the exit
  2. How many steps — one cover command (blocked) → one direct Alpaca call (succeeded)
  3. What doesn't work — you cannot close this position with the tool alone. Once a position larger than 8% is open, the tool's cover is blocked forever. You can get out by splitting the quantity — but a human has to notice and do it by hand

4.5 Where is the "scheduled autonomous operation"?

The README leads with "3-cron autonomous pipeline" and "Automation — Manual runs → 3-cron autonomous pipeline." I went looking in the code.

What I searched for Result
APScheduler · schedule · crontab · systemd · launchd 0 hits
A resident loop (while True + sleep) that cycles runs 0 hits
An Alpaca /clock call (is the market open?) 0 hits
Docker docker-compose.yml is a one-shot run
cron mentions in markdown README 28 lines · PLAYBOOK 23 · CRON-ARCHITECTURE 15 · BRIDGE-DESIGN 3

There is no scheduler. And what does exist isn't what you'd expect. Here's the shape of what the README writes down.

openclaw cron add --name swing-midday \
  --cron "0 12 * * 1-5" --tz "America/Los_Angeles" --exact \
  --agent swing-agent \
  --message 'Midday analysis.
    1. Gather data: poetry run python gather_data.py
    2. Analyze universe stocks for swing mode
    3. Execute: echo trades | poetry run python execute_trades.py --mode swing
    4. Report: trades executed, rejections from risk manager.'

What's on the timer is prose, not a command. A separate product called openclaw — not in the dependencies — wakes another LLM agent at the appointed hour, and that agent reads these English instructions and picks which scripts to run. "Analyze universe stocks for swing mode" is not an executable line. It's a sentence you'd say to a person.

So the precise meaning of "autonomous operation" is this: an LLM on a timer reads a to-do list written in English and does the typing for you. That isn't bad design. But you should know going in that this is not something you clone and leave running. Building the schedule is your job.

Tied to that: there is no market-hours gate in the code. Nobody calls Alpaca's /clock, so if a run fires on a weekend or after the close, the orders just queue up. The * * 1-5 in the cron expression is the only line of defense.

4.6 Scenarios I didn't run


5. Connecting to weltiq

5-1. I tried to carry it across, and got rejected

The procedure is the same as in other parts: move the tool's numbers into a rule document, write down the reasoning, and create a slot with stop conditions. What I was carrying was the tool's highest-confidence verdict — COIN short, 52 shares, 94% confidence.

The tool didn't emit levels, so I recomputed them with the same formula its own code uses: stop = reference × (1 + 0.07), target = reference − stop distance × 3.0. I invented no numbers.

POST /v1/strategy-rules rejected it with a 422 — {"code":"invalid_rules","detail":"unsupported_direction"}. Every number the tool produced is intact (short entry 52 shares, stop 212.09, target 156.59); what's missing is a short direction in weltiq's us-equity venue

Short is open in exactly three venues on weltiq — binance-usdm, bybit-linear and cme. US equities are long-only.

So there is no way to approximate this verdict. Swapping the ticker for a long makes it my judgment rather than the tool's, and flipping the direction is worse. When a column doesn't exist on our side, the rule for this series is to record that as it stands, so that's what I'm doing.

Zero longs across twenty-one tickers (§4.2) is what costs us here. If even one buy had come out, this section would carry a fill record. Today it doesn't.

5-2. But this part connects in the opposite direction

In this series, section 5 usually reads the same way: the tool produces judgment but can't place orders, so weltiq fills that gap. This part is inverted.

This tool weltiq
US equity short Yes No
Single-position cap on a new position Not enforced (§4.3) Mandatory. Without it, slot creation is refused
Stop on a new position Not attached (§4.3) Refused if the rule document has no levels
Stop watch on a short position Blind (§4.3) Direction-agnostic
Exiting at the size you entered Blocked (§4.4) Entry caps aren't re-applied to liquidation

One column weltiq can't do; four the tool can't. And all four of the tool's gaps are on the "what happens after you're in" side.

So the way to use this tool on top of weltiq comes out like this.

5-3. The homework this part leaves on our side

One honest note. This tool actually places US equity shorts. Measuring that judgment requires a column weltiq doesn't have. I'm filing it as "a column the tool wanted that we don't have."

What did hold, as in every other part, is the thread that runs through the execution stages. Rejected or accepted, the document is recorded by doc_hash, and what was refused and why stays in the record next to the code. The live stage is still not open — "the same document carries straight through" is what's true, and the rest is planned, with the promotion ladder as the gate.

(This article created no slot on our side. The rule document was refused with a 422 before slot creation, so there's nothing to stop or delete. The tool-side Alpaca paper position was closed after 74 seconds and the account is empty.)


6. Having used it

What was good

What was disappointing

Who it fits, and who it doesn't

Fits — anyone who wants to run multi-agent analysis without paid data. And anyone taking the verdict and attaching it to their own execution layer. The judgment-producing half is clean and comes with reasoning.

Doesn't fit — anyone planning to trust this tool's safety rails and leave --execute running. Don't turn it on until §4.3 and §4.4 are fixed. And anyone trading Korean stocks or crypto.


7. Next time

Next up is alex-jb/orallexa-ai-trading-agent. If this part's tool promised brackets and didn't attach them on new positions, the next one has real bracket code that its autonomous path never passes the stop to. It's also the tool in this series with the best-equipped machinery for folding confidence into size — and after watching 94% and 58% get treated identically here, that's exactly the next question.