DeepFund — six analysts produce signals and a manager decides the share count
Installing the LLM fund-manager benchmark an HKUST lab took to NeurIPS, then asking it the same question about one US stock six times to find out where 'buy 43
Part 30. DeepFund — six analysts produce signals and a manager decides the share countDone when: you receive a judgment on one symbol that says how many shares to buy and why. And half of this part goes into separating which parts of that judgment the model decided and which parts are division.
The verdict card
| What it does, in one line | Six analysts issue bullish/bearish/neutral signals on one US stock, a risk officer sets the allocation, and a manager returns buy/sell/hold + share count + reasoning |
| Who it fits | Anyone who wants the whole chain of reasoning (who issued which signal, how the allocation was set) kept as a record. Anyone who wants to replay the past one day at a time |
| Who it doesn't fit | Anyone who wants an entry, a stop and a target (you don't get them). Anyone looking at crypto or Korean equities (US stocks only). Anyone who wants to start without a key |
| What you end up with | The line Decision for NVDA: Buy \| Shares: 43 \| Price: 227.38 \| Justification: …, and the two signals that produced it, sitting in a local database |
| What you need · cost | Code free (MIT) · market and fundamental data requires an Alpha Vantage key (free tier: 25 requests/day, sign-up required) · model about $0.04 per run (estimated) · storage: a Supabase account or local SQLite · weltiq separate |
| Time · difficulty | 10 minutes to install, 15–20 seconds per run. Difficulty medium (a Python virtual environment and editing YAML) |
| Relationship to weltiq | It connects — but only halfway this time. Direction and share count moved into a rule and attached to the engine; the fill could not be seen because the market was closed |
| Test environment · date | macOS 15 · Python 3.13 · early morning 2026-09-22 (Korean time) · symbol NVDA · reference date 2026-09-21 close |
| Disclosure | I have a partnership with weltiq and am paid to write this series. Only measured results go into tool assessments, and where weltiq loses, I write that down too |
Every command and output in this article came from real runs. The numbers are untouched. The
Buy/Sell/Holdthis tool produces is that tool's output, and I say nothing about whether it is right. All orders are simulated. The raw record is inreports/a/30-deepfund/.
1. What this is
HKUSTDial/DeepFund is an instrument built to measure whether an LLM can run a fund. It came out of a lab at HKUST, and the paper was accepted as a poster in the NeurIPS 2025 datasets and benchmarks track. The paper's title tells you the tool's character directly — "Time Travel is Cheating". The point is to stop a backtest from using information that was not knowable at that moment.
The structure has two layers.
| Layer | Who | What it produces |
|---|---|---|
| Analysts | technical, news, insider, fundamental, macro, policy — 6 of them | bullish / bearish / neutral plus a short rationale |
| Managers | 1 risk officer → 1 portfolio manager | An allocation (0 to a cap, in 0.05 steps) → buy/sell/hold + share count + reasoning |
One thing in the README does not match reality. It says the data source is "Alpha Vantage or yfinance", but everything the code actually calls is Alpha Vantage. The yfinance class has a single news function and empty price functions, so choosing that path stops dead at the price lookup. Which means this is not a tool that runs without a key. That is why it is at the top of the card. (To write this article I filled in those two empty functions myself and ran it without a key — declared at the end of section 2.)
| Item | Value |
|---|---|
| Repository | HKUSTDial/DeepFund (★296) |
| Last commit | 2026-03-18 |
| License | MIT |
| Python required | 3.11 or above |
| Markets supported | US equities only (there is an adapter file for China A-shares, but it is not wired into the router) |
2. Installing
One verified path only. It uses uv.
git clone https://github.com/HKUSTDial/DeepFund.git
cd DeepFund
uv sync # creates the virtual environment and installs dependencies, 1–2 minutes
Put the data and model keys in. Copy the provided example file and edit it.
cp .env.example .env
# ALPHA_VANTAGE_API_KEY=... (free from https://www.alphavantage.co, sign-up required)
# OPENAI_API_KEY=... (or DeepSeek, Anthropic or Ollama instead)
# DB_PATH=src/assets/deepfund.db
Storage defaults to Supabase. If you don't want to create a cloud account, go with local SQLite. That is one account fewer.
cd src
uv run python database/sqlite_setup.py # creates 4 tables
Where it gets stuck
| Symptom | Cause | What to do |
|---|---|---|
TypeError: makedirs … NoneType |
You did not set DB_PATH |
Add DB_PATH to .env |
| Stops at the price lookup | No Alpha Vantage key, or the day's request quota is used up | Check the key. One symbol, one run costs 13 requests (see below) |
| Ran the default config and the daily quota was gone | The default config file is 2 symbols × 6 analysts = 26 requests | Start with one symbol and two or three analysts |
Supabase … connection |
You left out --local-db |
Add --local-db at the end of the command |
The free tier allows 25 requests a day and one pass of the default config file is 26 requests. Trim the config you start with down to one symbol and two or three analysts.
The screen after a successful install
uv sync plus the local SQLite initialization is the whole install. SQLite database initialized and four tables (config, portfolio, decision, signal) means it is done. The red line at the bottom is what catches people most often — leave DB_PATH out of .env and it dies right there.
To declare: the six runs in this article did not go through Alpha Vantage. I keep a rule about not creating accounts, so I never issued a key. Instead I wrote the two price functions (
get_daily_candles_dfandget_last_close_price) into the empty yfinance path described in section 1 and ran it that way. For news I used the yfinance function already in the repository. The patch is inraw/author_patch.diff.What that means for you: follow the install above with an Alpha Vantage key and run it as shipped, and the numbers will not match this article, because the data source differs. What differs is the data; the structure sections 3 and 4 describe — that the price field is overwritten with the close, that the prompts have no symbol name in them — is the same regardless of data source.
3. First result
Create one config file.
# src/config/my.yaml
exp_name: "my-first-run" # a different name per experiment — the same name continues the same book
cashflow: 100000
tickers:
- NVDA
planner_mode: false
workflow_analysts:
- technical
- company_news
llm:
provider: "OpenAI"
model: "gpt-4o-mini"
Run it.
cd src
uv run python main.py --config config/my.yaml --trading-date 2026-09-21 --local-db
This is what it looks like when it works.
INFO - Active analysts for NVDA: ['technical', 'company_news']
INFO - Agent: technical | Ticker: NVDA | Signal: Neutral | Justification: Bullish trend-following is
offset by a bearish volume trend … At $227.38, NVDA is only 0.26% below resistance at $227.98
versus approximately 0.69% above support at $225.83 …
INFO - Agent: company_news | Ticker: NVDA | Signal: Bullish | Justification: Rackspace joining
NVIDIA's Cloud Partner Program supports ecosystem expansion … Alibaba's proprietary AI chip
poses longer-term competitive risk.
INFO - Risk Control for NVDA | Optimal Position Ratio: 0.1 | Justification: Recommend a cautious 10%
NVDA allocation … allocate $10,000 and retain $90,000.
INFO - Decision for NVDA: Buy | Shares: 43 | Price: 227.3800048828125 | Justification: Buy 43 shares
based on the supplied positive tradable-share amount and zero current holdings, for
approximately $9,777.34 before fees.
INFO - DeepFund run completed in 18.32 seconds
A run in progress
This is the tool at work. Top to bottom: the two analysts (yellow = neutral, blue = bullish) → the risk officer's allocation of 0.1 → the manager's final line (green). Look at Shares: 43 and Price: 227.38 in the green line — the paragraph immediately below is about the fact that those two come from completely different places.
18 seconds, one symbol, one line of judgment. The same content lands in the local database's decision and signal tables, together with the raw prompts. This tool's real output is not the screen; it is those tables.
There is no dashboard. No web UI, no TUI, no charts, no address to open — the terminal log and those four tables are all of it. Go with the repository default of Supabase and you get Supabase's own table view, but that is not a screen this tool built and it requires an account. If you went with --local-db, looking at the tables directly with sqlite3 is how you look at this tool.
What Price: 227.38 actually is — the most important paragraph in this part
Because a price comes out alongside the judgment, it is easy to read it as "the model said to buy at 227.38". It is not.
After the model fills in the price field and returns it, the code overwrites that value with the previous close. The instruction in the prompt is to write down the current price in the first place; the model was never asked to choose an entry. Across the six runs above, the price field was 227.3800048828125 every time — identical to NVDA's 2026-09-21 close down to the last decimal.
The share count is much the same. Take the risk officer's 10% allocation, multiply by $100,000 of capital, divide by the close: 100000 × 0.10 ÷ 227.38 = 43.98 → 43 shares. The pass with 15% gave 15000 ÷ 227.38 = 65.97 → 65 shares. The manager's reasoning sentence even says it followed the tradable share amount it was given.
To summarize:
| Field | Who decides it |
|---|---|
| Direction (buy/sell/hold) | The model |
| Allocation (0.05 steps) | The model (the risk officer) |
| Share count | Division — allocation × capital ÷ close |
| Price | Overwritten by the code with the previous close |
| Stop and target | None |
The judgment this tool produces is "how much", not "at what price". Shipping a number that must not be used as an entry price inside a field that looks exactly like an entry price is this tool's biggest trap, and if you wire it into automation without knowing, it is quietly wrong.
4. Getting the most out of it — put the symbol name into the prompt
The first three runs were hold, 0 shares, all three times. Reading the reasoning, I found the same sentence.
"No target ticker was specified, and the headlines span multiple companies, preventing a reliable company-specific assessment."
I opened the prompts, and it was true. Nowhere in the six analyst prompts or the two manager prompts does the symbol name appear. The English word "ticker" simply sits inside the sentence like a placeholder. On the technical analysis side it is even clearer — the code is passing the symbol name into the prompt, but the prompt text on the receiving end has no slot to receive it. It is a half-finished spot.
I changed ticker to {ticker} in four prompts and made the two places that were not passing the symbol name pass it. The sentence structure is unchanged. Then I ran it three more times under identical conditions.
| Run | After/before the fix | News signal | Allocation | Decision |
|---|---|---|---|---|
| 1 | before | neutral ("no ticker specified") | 0.00 | hold, 0 shares |
| 2 | before | neutral ("no ticker specified") | 0.00 | hold, 0 shares |
| 3 | before | neutral ("no ticker specified") | 0.00 | hold, 0 shares |
| 4 | after | bullish (NVIDIA partner program, IPO) | 0.10 | buy 43 shares |
| 5 | after | bullish | 0.10 | buy 43 shares |
| 6 | after | bullish | 0.15 | buy 65 shares |
The six runs as they sit in the local database
In table form this section's story is visible at a glance. In the signal table above, only the company_news column turns green (bullish) from run4 onward — technical is neutral all six times. The decision table below is the consequence, and the price column on the far right reading 227.3800048828125 on all six rows is the overwrite described in section 3.
Technical analysis was neutral all six times (that side receives numbers, so it computes fine without a symbol name). What split was the news analyst. Without the symbol name it looked at the same bundle of headlines and gave up, saying it could not tell which company was being discussed; given the name, it picked the NVIDIA-related items out of the same headlines and went bullish.
That one word is the difference between doing nothing and buying $9,777 worth. If you intend to use this tool seriously, this is the place to fix first. Note clearly, though, that the judgment carried into section 5 below came out of code I modified.
I also checked the parts that behave like research code. Prices and insider trades are truncated after the reference date, and if you run the reference date backwards under the same experiment name it refuses to execute. It earns its paper title. But the fundamental and macro indicators have no such filter — they fetch the latest values whatever the reference date is. The tool's own technical documentation says so. Turn those two on while replaying the past and that judgment is looking at information nobody had that day.
5. Moving it into weltiq — an integration experiment
weltiq is the side that freezes a strategy into a rule document and runs it 24 hours a day with simulated fills, costs and a journal attached. If DeepFund decides what to buy, this side measures what it actually fills at and what leaks out. I moved run 4's judgment (buy 43 shares of NVDA) over.
The move took five steps.
- Pick the fields that can move. Only direction (buy) and share count (43). The tool produces no entry, stop or target
- Write the entry condition. There isn't one, so I used "enter if the held quantity is 0" — the closest thing to the tool's behavior of issuing a buy when flat
- Attach a protective condition. The 3% stop is a value I chose. Resident execution requires a protective condition, and the tool has no concept of a stop at all
- Write the reasoning in my own words. This is where I state what is the tool's judgment and what is a field I filled in. Without it, registration is rejected
- Create the strategy container and start it. A maximum loss and an end time go on at the same time
POST /v1/strategy-rules → rule document settled ("(position.size = 0) → enter long · protective stop 3% · quantity 1")
POST /v1/ai/thesis → reasoning recorded
POST /v1/containers → status = running, attached to the engine
That is what was confirmed, and what follows is what was not. I ran this in the small hours Korean time, so the US market was closed. The container attached to the engine, sat in "waiting for a candle", and stopped itself at the appointed time. Zero fills. That is why this section is titled an integration experiment.
Two fields were lost on the way. Both are written down here.
| Field lost | Whose problem it is |
|---|---|
| 43 shares → 1 share (at the time) | weltiq's. Back then rules uploaded with an external key had an order quantity cap fixed at 1 share, so 43 was rejected outright and the allocation information in the judgment disappeared here. Fixed on 2026-09-23 — the rule document's sizing.qty is now the order cap itself, so 43 shares go through as given. What follows is the record of the day I cut it to 1 |
| No entry condition | DeepFund's. "When to buy" is not in the judgment, so I filled in "when flat". That is not the tool's judgment |
The sentence to keep from this section is this. When DeepFund's judgment is moved into a rule, half of it passes through my hands. The direction and the share count go straight through (the share cap is gone since), and the entry and stop were never there, so they have to be written fresh. If you want a judgment executed as issued, the right tool is one that produces an entry line and an invalidation condition alongside it — not this one.
6. After using it
What was good
- The whole reasoning chain is kept. Six signals, the allocation and the final decision all land in tables, raw prompts included. Tools that let you go back and ask "why did I buy then" are not common even in this series
- The judgment does not wobble. Three runs under identical conditions gave the same direction all three times (hold three times before the fix, buy three times after). Only the allocation moved one step, 0.10, 0.10, 0.15
- Replaying the past is in the design. The structure carries the book forward as you move the reference date a day at a time, and it blocks you from running it backwards
- 15–20 seconds for one symbol, one run. Among the fastest in this series
- Swapping models is easy. Eight providers are already in there, and a local model is one of them
What fell short
- The price field is not a judgment. It looks like an entry price but it is the previous close written over the top. Use it without reading the section 3 paragraph and you are quietly wrong
- There is no stop and no target. Risk management is "allocation" and nothing else
- The prompts have no symbol name in them. That is what section 4 is about. Before the fix, the news analyst cannot do its job
- It does not run without a key. The free path the README describes is not implemented in the code, and the free tier's limit (25 requests a day) cannot even cover one pass of the default config (26 requests)
- It is not reproducible. Model temperature is fixed at 0.5 and there is no seed. The paper's title blocks time travel; it does not block the same input producing a different answer
- Fundamental and macro indicators have no point-in-time filter. Turn those two on while replaying the past and values nobody had that day get mixed in
Cost table
| Item | Measured / confirmed |
|---|---|
| Code | Free (MIT) |
| Data | Alpha Vantage key — free tier 25 requests/day. Up to 13 requests for one symbol, one run |
| Model | About $0.04 per run (estimated — the tool does not report usage, so this is computed from the stored prompt and response lengths) |
| Storage | A Supabase account (default) or local SQLite (free) |
| Time | 10 minutes to install · 15–20 seconds per run |
7. What to do next
Three things you can do right now.
- Start with one symbol and two analysts. Run the default config file as-is and the free data quota is gone in a single pass
- If your first run comes back "hold, 0 shares", read section 4 first. The tool may not be being cautious — it may simply not have seen the symbol name
- Do not use the price field as an entry price. What this tool gives you is direction and allocation. When to go in is yours to decide
And once you have decided that "when and at what price", it is time to measure how that rule behaves on top of real fills and real costs.
weltiq setup
- Sign up — simulated only, no live orders
- My → Connections → issue a key
- In a terminal,
claude mcp add weltiq --header "Authorization: Bearer <key>" …You can freeze a judgment into a rule and run it with fills, fees and slippage attached.