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
- What came out — zero paid hosts.
api.financialdatasets.ai($200/month) is still in the tree, sitting insrc/tools/api_original.pywith nothing importing it. The livesrc/tools/api.pyis a nine-line file that re-exportsapi_free - How many steps — six function calls. Zero keys
- 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
- What came out — twenty-one verdicts. Seventeen short, four hold. A reasoning sentence attached every time
- How many steps — one command per run. Three tickers in 1 minute 45 seconds, six in 2 minutes 30 seconds
- 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
- What came out — one real fill (75 AMZN short at 249.25), and three safety layers empty in a row
- How many steps — one command, exactly as the tool's own documentation instructs
- What doesn't work — all of the above. But the dry-run default is genuine: without
--executenothing 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
- What came out — the only case where a stop condition actually fired. And it fired at the exit
- How many steps — one cover command (blocked) → one direct Alpaca call (succeeded)
- 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
- S1, a single global ticker (BTC-USD): not possible with this tool. Every run printed
Quote not found for symbol: BTCUSD. The universe is hardcoded US tickers, quantities are always whole shares, and the broker is Alpaca equities paper. There's no crypto path - S2, a Korean ticker: not possible with this tool, for the same reason
- S3, replaying my own execution record: this tool reads only its own snapshots. There's no inlet for an external export
- S6, one full loop: I couldn't close it. Analysis → order → fill worked (§4.3), but I never got back around to amending the rule and re-ordering, because the exit was blocked inside the tool (§4.4). The record of where the loop broke is the result of this run
- S8, disconnect and reattach: not run. There's no resident process, so there's nothing to disconnect (§4.5)
- S9 fee re-measurement · S10 head-to-head: not run. Those belong to the B-track rounds
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.
- Short verdicts can't be carried. Not while US equities are long-only
- When a buy verdict does come out, it's worth a lot — the caps, stops and monitoring the tool doesn't attach are all things the slot holds. The tool's weak spot and weltiq's strong spot overlap exactly
- What this combination doesn't give you: the slot doesn't fold the tool's confidence into size. Whether it's 94% or 58%, the share count carried across is whatever the tool produced
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
- The data really is free. Of everything I measured here, this is where the README's claim held up most cleanly. Zero authentication headers, zero paid hosts, and the per-run cost is LLM tokens only ($0.003984 for three tickers). Replacing a $200 subscription with SEC EDGAR and Yahoo is this fork's genuine contribution
- The default is a dry run. Without
--executenothing reaches the broker, and the Alpaca address is hardcoded to paper — there's no live URL in the code at all - Every verdict carries reasoning.
reasoningis a required field, so it can't come back empty, and all twenty analysts produce their own - You can write your own agents.
--analystschanges the roster, and there's a documented path for adding a new personality
What was disappointing
- The "code-enforced risk management" is entirely empty on new positions. The 8% cap, the sector cap, the cash reserve and the mandatory stop all pass on a reference price of zero (§4.3). The cause is a single thing — the price falling off at a process boundary — but the effect demolishes the README's two biggest claims at once
- Nobody watches the stop on a short position.
portfolio_monitor.pyfilters onqty > 0(§4.3) - You can't exit at the size you entered. The tool's cover blocks any position over 8% (§4.4)
- The README diverges from the code in four places — "the day account is optional" (§2.1), the "paid→free fallback hybrid" (§4.1), the "3-cron autonomous pipeline" (§4.5), and the impression that short support and stop monitoring mesh
- There are two entry points and they behave differently.
run_analysis.pyis a near-duplicate ofrun_hedge_fund.py, but it executes in-process, doesn't apply the mandatory swing bracket, and defaults to a different model. It's a trap where the same mission gets a different code path - No push in six months, and no license file
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.