ai-hedge-fund from install to a real order — why a 60,000-star AI hedge fund places no trades
Installing virattt/ai-hedge-fund in 4 seconds, finding out how far it runs with no model key, attaching investor agents through OpenRouter, and measuring whethe
Part 2. virattt/ai-hedge-fund — why a 60,000-star AI hedge fund places no tradesDone when: you have brought up, on your own terminal, the screen where Buffett and Graham take opposite positions on the same stock, and you know how empty the spot is where that judgment turns into an order.
| What it does | Dresses an LLM in the persona of a famous investor, has it form views, and blends those views into a portfolio. It judges and it does not order |
| Fits you if | You want to build fundamental judgment structurally, and to swap parts and experiment with "what changes the result" |
| Not for you if | You need an order to go out right now · you trade crypto or Korean stocks (US equities only) |
| What you get | One judgment report with the views split, an equity curve drawn over the S&P 500, and the return difference when the data is swapped for a free source |
| What you need · cost | Code $0 (MIT) · Model one of six providers (this article used OpenRouter, 4 paid calls for $0.836381. Use only the quantitative model and it is $0) · Market data Financial Datasets, paid ($20 / 1,000 requests; this article used 80) · weltiq only for section 5 |
| Time · difficulty | 4-second install, 13 seconds to a first result. Difficulty medium (a virtual environment and exporting environment variables) |
| Relation to weltiq | An integration experiment. The judgment is a target weight, so a human has to rewrite it as a rule, and this article never got as far as a fill (section 5) |
| Verified on | macOS · Python 3.12 · aihf 2.3.0 · 2026-09-21 and 22 |
| Affiliate disclosure | I am an affiliate contributor to weltiq. This tool is third-party open source unrelated to weltiq, and every number below I obtained by running it myself |
This is the most-starred tool in the series. ★63,653. And the second line of the README reads — "Note: the system does not actually make any trades."
1. What this is
What matters about virattt/ai-hedge-fund is that it is a desk, not a bot. The author transposed the hierarchy of a real hedge fund straight into code.
FUND = splits capital across several STRATEGIES (master risk on the combined book)
STRATEGY = a policy that blends the views of several MODELS ("pod")
MODEL = one alpha model → one Signal (conviction −1~+1 plus reasoning)
Two kinds of thing slot into that MODEL position through the same interface. LLM investor personas (Buffett, Munger, Graham, Lynch, Druckenmiller), and pure Python computation (PEAD — a quantitative model that plays the post-earnings-announcement drift). Use only the quantitative model and the fund runs with no model key.
The design principles are nailed into the repository — "The LLM never touches the trade." Agents only form views; deterministic code sizes and places the orders. It judges and it does not order. brokers/ holds only SimBroker, and the module table notes for itself "paper/live brokers planned". That empty spot is section 5.
| Item | Value | How it was checked |
|---|---|---|
| URL · license | github.com/virattt/ai-hedge-fund · MIT |
GitHub API |
| Stars | ★63,653 · 11,159 forks (observed 2026-09-22) | GitHub API |
| Last push | 2026-09-18 · created 2024-11-29 · 166 open issues | GitHub API |
| Package | PyPI aihf 2.3.0 · Python >=3.11,<4.0 |
PyPI JSON API |
| Alpha models · brokers | 6 (5 LLM + 1 quantitative) · SimBroker, one |
hedge_fund/ source |
2. Installing
⚠ This tool is not free
Financial Datasets (prices, fundamentals, earnings) is $20 / 1,000 requests, prepaid. This whole article used 80 requests. The model API costs $0 if you use only the quantitative model, but the data key is needed either way.
The README points at pipx and poetry; my machine had neither, so I installed with uv.
git clone https://github.com/virattt/ai-hedge-fund /tmp/ai-hedge-fund
uv venv --python 3.12 /tmp/aihf-venv
uv pip install --python /tmp/aihf-venv/bin/python /tmp/ai-hedge-fund
4-second install. 0.024 seconds for the virtual environment plus 4 seconds for the packages (clone excluded). You do not have to put the keys in ahead of time — the moment one is first needed, the app asks and saves it to ~/.hedge-fund/.env with 0600 permissions.
Where it gets stuck
| Symptom | What it is | Way around |
|---|---|---|
Invalid end_date: end_date must be today (2026-09-21) or older |
Type it in Korea and you get this almost every time. The screen's "today" is the machine's local date while the data API's "today" is the US date | Launch with TZ=America/New_York aihf, or pass --date explicitly |
abstained: … only 3 filed periods (need 4) |
Not a malfunction. If the snapshot does not hold 4 quarters, the agent refuses to form a view | Leave it. An abstention belongs in the record |
Backtest shows n_fills: 0 with no error |
The size setting works out below 1 share. Stocks trade in whole shares | Raise the size |
poetry: command not found |
The README assumes poetry | uv pip install works |
GET /news/ 400: Invalid limit |
The server rejects the news default of limit=1000. A bug on the tool's side |
Nothing in the current pipeline uses news, so runs are unaffected |
The first one is worth a picture.
A run killed by the time zone
All three agents say Done and there is one red line at the top. end_date must be today (2026-09-21) or older — my machine was already on 9/22. Korean and Japanese users meet this first.**
3. First result
3-1. The screen is a terminal app
Type aihf with no arguments and an interactive terminal app (TUI) comes up. It is not a web app, so there is no address.
The first aihf screen
Right after typing aihf with not a single key entered. Only two verbs — run a fund, or build a new one. The line below reads agents reason with Opus 5 claude-opus-5 — even with no key, a default model is already selected.**
With no key the home screen, the model picker and the fund-building wizard all open. The one place it stops is the moment you press Enter to run. Even on first open an example-fund is installed, so Run an existing fund → the example → a ticker → Enter takes you to a first result. The captures are 140×42, but it does not break at 80×24 either (capture).
The model picker
The model list you get by pressing m. With no key, the list still opens grouped by provider (seven providers, nine models). The openai/gpt-6-astra on the OPENAI row is a name I edited in myself in 3-3.
Asking for the data key
The result of putting AAPL into the example fund and hitting Enter. It asks for the data key before the model key — with no prices there is nothing to call an agent about.
3-2. One fund cycle with no model key
A mandate is just YAML, and what is distinctive is that it names no tickers — "a mandate is a desk, not a watchlist." Tickers are supplied at run time with --tickers.
# ~/.hedge-fund/mandates/quant-only.yaml
name: quant-only
strategies:
- {name: earnings-drift, weight: 1.0, models: [{name: pead}]} # pead = the quantitative model. No LLM
risk: {max_position_pct: 0.25, max_gross_exposure: 1.0}
capital: 100000
rebalance: weekly
benchmark: SPY
aihf ~/.hedge-fund/mandates/quant-only.yaml \
--tickers AAPL,MSFT,NVDA --date 2026-09-18 --out cycle.json
This is what success looks like.
quant-only @ 2026-09-18 · 1 strategies · 3 signals · 0 risk clamps ·
0 orders · NAV $100,000.00
It took 13.2 seconds and used 6 Financial Datasets requests. Not one model-key error. 0 orders is not a malfunction — PEAD forms a view only when an earnings release fell within the last 4 days, and all three tickers were quiet that day. The full record goes to stdout as JSON, so | jq works straight away.
3-3. Attaching OpenRouter, which isn't on the list, in one line
The tool states support for six providers and OpenRouter is not among them. But one door is open in the source — at hedge_fund/llm/client.py:259, only the OpenAI branch reads base_url from an environment variable (the Anthropic branch does not take that argument).
export OPENAI_API_KEY="<OpenRouter key>"
export OPENAI_API_BASE="https://openrouter.ai/api/v1"
And here is the trap. The name in the tool's model list (api_models.json) is gpt-6-astra, while OpenRouter requires a vendor prefix. Querying the 440 models (observed 2026-09-22) gave: openai/gpt-6-astra yes, gpt-5.6 and openai/gpt-5.6 no. And this tool sends any name it does not recognize to Anthropic, so exporting the name alone makes it die looking for an Anthropic key. The fix is one line — change that entry's model_name in the installed list file to openai/gpt-6-astra and the name that goes out is one OpenRouter understands (replacement script).
I measured the cost too. The calls that cost money number 4; every run after that was a full prompt-cache hit. OpenRouter usage went 16.311928679 → 17.148309638, an increase of $0.836381 — roughly $0.21 per call. The accounting lags by a few minutes: right after the run it read $0.706238, later $0.836381.
4. Getting the most out of it
4-1. I checked at the byte level whether the backtest really is "the same code"
The README's claim — "a backtest is … the fund, replayed, not a separate simulator." I measured it. With the same mandate and tickers I ran (a) a single cycle for 2026-07-02 and (b) a backtest of 2026-07-01 to 09-18, then hashed the record for the first rebalance date inside the backtest against (a), both as sorted JSON.
backtest first record as_of=2026-07-02 sha256=6dfdd25eee917d1eb95ad6801a9255514e19b6b7ffedb8ab4ca261effc76e806
standalone one cycle as_of=2026-07-02 sha256=6dfdd25eee917d1eb95ad6801a9255514e19b6b7ffedb8ab4ca261effc76e806
identical=True
Byte-identical. Mark prices, signals, reasoning, risk clamps, orders and cash, all of it. The claim is true.
The equity curve
quant-only (one quantitative model, run with no model key) over 2026-07-02 to 09-18 with weekly rebalancing. Green area is the fund, blue line is SPY. In the fill tape below, look at how the orders cluster into only two stretches out of 12 weeks — bought AAPL and MSFT on 7/31, sold 8/7; bought NVDA on 8/28, sold 9/4. That is the obvious shape for a rule that moves only right after an earnings release.
Period 2026-07-02→09-18, 12 cycles. Return +3.72% (SPY +2.27%, excess +1.45%) · Sharpe 3.01 · max drawdown 0.00% · 6 orders.
I claim nothing from these numbers. It is a sample of 6 trades in 12 weeks, and a max drawdown of 0% means "it was in cash most of the time." And these fills come from SimBroker — 100% filled exactly at the reference price, zero slippage, zero fees. That point is the starting line for section 5.
Attach the agents and the screen changes like this.
Views from the investor agents
One cycle of the hybrid-desk mandate as of 2026-09-21, mixing a quantitative pod (Earnings Drift 40%) with a discretionary pod (Deep Value 60%). On the same AAPL, Buffett came in at — 68% (neutral) and Graham at ▼ 94% (bearish). On the right is the Buffett persona's reasoning, verbatim.
Graham's dissent
The Graham row. Bearish with 94% conviction — "at 34.98 times earnings and 41.91 times book, this is a quote with no margin of safety by my conservative standards." This is that tool's output, not my opinion or weltiq's.
The fund-building wizard
The Build a new fund screen. The left rail holds four steps — name, strategy, capital, rebalance frequency. Note that there is no step asking for tickers.**
Picking a strategy
Picking a strategy. On the right of each row it says who sits in that pod — Deep Value has Graham, Buffett and Munger; Earnings Drift has PEAD alone. The list is read from the YAML in hedge_fund/strategies/, so drop in one file and you have one more strategy, with no code.**
4-2. Can free yfinance save the $20
The thing this tool does best is swapping parts. The data layer requires no inheritance — match the method names and the pipeline takes it. I wrote the yfinance client the documentation gives as an example (yf_client.py, 254 lines), and attaching it is one line.
fd = CachedDataClient(YFinanceClient(), cache_dir="~/.hedge-fund/cache/yf")
result = backtest_fund(Fund(spec), "2026-07-01", "2026-09-18", fd, ["AAPL","MSFT","NVDA"])
Prices are identical.
Closing prices from both sources
AAPL daily closes lined up to six decimal places. The diff column is all 0.00001 or below — the difference is not in the values but in floating-point representation (yfinance uses float32). One exception on the bottom line: volume does differ — on 9/18, FD 86,588,203 vs yfinance 86,433,100.
Across all three tickers (2026-07-01 to 09-18, 56 trading days) the candle count matched 56/56, no dates missing, max absolute close difference AAPL 0.000015 · MSFT 0.000029 · NVDA 0.000007. yfinance even responded faster (AAPL 975ms vs 553ms). On prices alone the free one wins.
Where they diverge is "when it became public." The tool's first design principle is "Point-in-time by construction … No lookahead, ever." — and that is where the paid one earns its place.
| Financial Datasets | yfinance | |
|---|---|---|
filing_date on financial-metric rows |
AAPL 4/4 · MSFT 4/4 · NVDA 4/4 | 0/5 · 0/5 · 0/5 |
| Actual filing lag (report period end → public) | AAPL 34, 34, 34, 34 days · MSFT 29, 29, 28, 29 days · NVDA 31, 24, 31, 24 days | unknown |
These numbers are the whole story. The quarter that ended 2026-06-27 became public 34 days later, on 2026-07-30. Use yfinance alone and for those 34 days you are making trading decisions on numbers that did not yet exist in the world. That is not performance, it is lookahead. (The full endpoint-by-endpoint comparison is in the run log.)
So I ran the same fund on both data sets. Same mandate, same tickers, same range, only the data provider changed.
| Financial Datasets | yfinance | |
|---|---|---|
| Rebalance grid | 12 cycles (identical) | 12 cycles (identical) |
| AAPL 7/31 buy | 80 shares @ $308.91 | 80 shares @ $308.91 |
| MSFT 7/31 buy | 53 shares @ $464.72 | 53 shares @ $464.72 |
| NVDA 8/28 buy | 117 shares @ $217.55 | none |
| Order count | 6 | 4 |
| Return | +3.7217% | +2.2229% |
| Sharpe | 3.0125 | 2.0817 |
AAPL and MSFT match to the cent and NVDA dropped out entirely. The reason was specific.
=== yfinance NVDA (as my client mapped it)
filed=2026-08-26 report_period=2026-06-30 lag=57d -> DROPPED by PEAD 45-day filter
=== Financial Datasets NVDA
filed=2026-08-26 report_period=2026-07-26 src=8-K lag=31d -> kept
Nvidia's fiscal quarters do not line up with calendar quarters — one ends 2026-07-26. Since yfinance gives no fiscal quarter, my client had to guess the calendar quarter end (6/30), which inflated the filing lag to 57 days and tripped PEAD's "over 45 days, drop it" rule. Apple survived because its gap is 3 days; Nvidia died because its gap is 26.
What is the $20 actually buying
| If you are | |
|---|---|
| only after charts and prices | yfinance is enough. Same values, faster, free |
| serious about backtesting | Pay. Without filing dates, an average of a month of lookahead quietly gets in |
| building an earnings-event strategy | Pay. Without the fiscal quarter and the filing-type distinction, a whole ticker disappears |
| only after a long history of announcement dates | yfinance goes further back. Back to 2020, free |
What the $20 buys is metadata, not prices. When it became public, which filing it was, which fiscal quarter it covered. Without those three, a backtest is quietly wrong — with no error, either.
5. Moving it to weltiq — an integration experiment
This tool's output is not a trigger but a target weight. In the 2026-07-31 cycle the quantitative pod said buy (PEAD AAPL and MSFT +1.0 each) and Graham on the discretionary pod said there was no margin of safety (MSFT −0.88 · AAPL −0.94); blended, the target weights were AAPL −0.353 · MSFT +0.153, and the risk limit cut AAPL to −0.25. The final orders were short 80 shares of AAPL @ $308.91 · buy 32 shares of MSFT @ $464.72.
Three lines of setup — sign up for weltiq ({{CTA_URL}}) → My page → issue a key under connection management. Then five steps.
- Check the market first with
GET /v1/capabilities—us-equity open, andlive_orders=False(every order in this article is simulated). - Turn the target weight into a quantity — MSFT +0.153 × $100,000 = $15,294, 30 shares at the share price.
- A human fills the two empty boxes. This tool says only "hold this weight" and says nothing about when to get in (it deliberately separates views from position mechanics). It has no concept of a loss limit either. The entry rule came from a weltiq template and I put the stop conditions in by hand ($300 = 2% of capital, 3 hours).
- Create the strategy container. Stop conditions are a mandatory gate — leave them out and it does not come up.
- Pull the record with
GET /v1/containers/{cid}/exportand feed it back into the next judgment.
POST /v1/containers
{"name":"a02-aihf-msft-r1","source":"template","template_id":"TPL-002",
"params":{"signal.n":20,"exit.sl_pct":"0.02","exit.tp_pct":"0.04","sizing.qty":"30"},
"symbols":["MSFT"],"venue":"us-equity","tf":"1m",
"balance":"15294","stop_max_loss":"300","stop_after_hours":3}
The numbers you carry over do not scatter as settings; they become one strategy document, and a hash is attached to it. Change the parameters and the hash forks too — the numbers this tool produced change that document's identity.
The weltiq strategy container screen
The detail screen for the run-2 container (a02-aihf-msft-r2). On the SYMBOLS line, the one-line status — Entry conditions not met — waiting (normal). It says in one sentence why nothing is happening. On the right, Auto-stop −300 · 9/22, 04:28 AM is the boundary I drew by hand.
That is the success path, and what follows is the conclusion of this part. While the US market was open (2026-09-21 13:14–14:15 ET) I brought the container up five times, fixing the rule each time (20-candle breakout → 5-candle → a 5/15 moving-average cross; 30 shares → 20 → 5; the ticker to NVDA as well), and all five times the order intents were 0. So I knocked on the boundary directly with a one-off order.
GET /v1/orders/estimate?venue=us-equity&symbol=MSFT&side=buy&size_unit=base&size_value=30
→ {"qty":"30","ref_px":"499.31","est_notional":"14979.30","fee_estimate":"0E-8"}
POST /v1/orders/oneoff {… "qty":"30", "thesis":{"why":"… PEAD BEAT +1.0 와 Deep Value 를 합쳐
MSFT 목표비중 +0.153 …"}, "invalidation":{"px_below":"489.32"}}
→ 422 {"code":"envelope_required","detail":"이 심볼을 허용하는 주문 한도 승인이 없습니다 — 먼저 승인하세요"}
The thesis.why above is the Korean text I sent, summarizing that cycle's blended MSFT target weight of +0.153; the refusal's detail is the server's own Korean, saying there is no approved order envelope permitting this symbol and that one must be approved first.
The quote passed with no trouble (30 shares, $14,979.30 notional) and it was stopped at the last box — because there was no human-approved order limit for that ticker. I also checked whether I could widen that limit with the API key, and got 403 (session_required). The only limit approved on this account was BTCUSDT ($500 per order), so US stocks were never open in the first place.
You could write this as "the loop did not close" or as "the boundary worked," and both are right. Approving a limit is something a human does in a browser, so I did not open it in this article. The observed range ends here — the order attempt, reading the result and fixing the rule all ran, and there was no fill. Stop conditions were set on all five runs (Auto-stop −300) but there was no fill that could have lost money, so the trip is not verified. Not verified is not a failure; it is not verified.
So what changed
this tool's execution (SimBroker) |
weltiq paper | |
|---|---|---|
| Fill price · fees | 100% at the reference price · fees none (source says "future addition") | quote-based, the model's share and the latency share recorded separately · deducted and recomputable via a rate profile |
| Rejections | none — no rejection path exists | kept with reason and count (entry_blocked, qty_below_lot, insufficient balance) |
| Human approval | none — the code is the authority | order-limit approval is a separate gate. Without it, envelope_required; widen it with an API key and you get 403 |
| Stop conditions · when it goes down | no concept of them · the position goes with it (a Python object) | a mandatory gate (without them the container does not come up) · it stays on the server ledger |
I will write down the cells weltiq loses too. This tool's backtest sweeps 18 months while keeping the reasoning text of every cycle whole, where a weltiq backtest leaves only fills and rejections. And the short views this tool produces (AAPL −0.25) cannot be carried into a us-equity container. Nor can netting several tickers into one book — this tool's single biggest feature is lost in transit.
One last thing. This tool's run_cycle docstring ends with "Only the clock and the broker change — the pipeline never does." — the same sentence weltiq wrote down as its own design principle. There is exactly one difference: this tool's PaperBroker and RealBroker do not exist yet ("planned").
6. After using it
What was good
- The design is honest. "The LLM never touches the trade," "only the clock and the broker change," "no lookahead" are written in the documentation and kept in code. Measured with hashes, it was real (4-1).
- Swapping parts is easy. Replacing the data provider took 254 lines and not one line of the pipeline changed.
- Refusals and abstentions stay in the record. "No view, insufficient data" is not a silent 0 but an
abstained. - The prompt cache saves money. The cache key is the hash of the fundamentals snapshot, not the date, so with no new filing a different date produces the same prompt — the 7/31 and 9/21 cycles used the same cache.
What fell short
- It places no orders. The README says so first, so there is no deception, but a thing named a hedge fund has exactly one broker,
SimBroker. - US stocks only. The alpha models all stand on SEC filings, so extending it is not easy either.
- The time-zone bug blows up on the first screen. Typed in Korea after 9am it dies with
Invalid end_datealmost every time (section 2). The news endpoint also gets a 400 with its default value. - It is expensive. 2 tickers with 2 agents = 4 calls at $0.836381. Five personas over 10 tickers is 50 calls, somewhere above $10 a cycle.
| Cost | Value |
|---|---|
| Code · time | $0 (MIT) · 4-second install · 13 seconds a cycle (quantitative) |
| Market data | Financial Datasets $20 / 1,000 requests — 80 requests for this whole article |
| Model | 4 paid calls, $0.836381 ($0.21 per call). Use only the quantitative model and it is $0 |
What this tool cannot do — crypto and Korean tickers, picking tickers for you (there is no screener), running resident (one cycle and it stops), re-measuring at different fees (SimBroker has no fee model).
7. Next steps
Start by running one cycle of the quant-only mandate — 13 seconds, no model key. Then swap the data provider for yfinance and you will confirm by hand where the return difference in 4-2 comes from.
Next is part 3, Erfaniaa/LLM-Auto-Trader. This part's tool judges thickly and leaves the order spot empty; the next one does both, with a very thin waist joining them — the place where it touches the exchange is thirteen CCXT functions, so a single file imitating those thirteen moves its execution somewhere else entirely.
weltiq setup — 3 lines 1. Sign up: {{CTA_URL}} 2. My page → issue an API key under connection management 3.
claude mcp add weltiq --header "Authorization: Bearer <key>" https://weltiq.ai/mcp
Every command, output and number in this article was actually run on 2026-09-21 and 22. No real money moved; everything is simulated, and none of it is trading advice. The "bullish/bearish" is that tool's output. The trial and error, raw logs and full comparison tables not written here are in the run log.