ALTA from install to real use — four LLM agents do the research, and code holds the execution boundary
The real path to standing ALTA up without Docker, the four places it got stuck and the four workarounds, what nine autonomous cycles produced, and a record of p
Part 11. ALTA — the agents judge, the code holds the execution boundaryDone when: you have stood the ALTA backend up without Docker, driven one autonomous cycle to completion, and — when the result is "bought nothing" — read out of the agents' own sentences why they did not buy.
| What it does | Four scout agents chase evidence with four different questions, two assessors who cannot see each other argue back, an arbiter surfaces the disagreement, code ranks what survives, and further agents choose the expression and audit it. All of it on your laptop |
| Who it fits | Anyone who wants to audit an agent's reasoning — who looked at what, argued from what, and who pushed back, all kept as records. And anyone willing to pay for paid data sources |
| Who it doesn't fit | Anyone who wants to place an order right now · anyone who does not use Docker · anyone who needs Korean equities · anyone who needs a price stop enforced as a rule (that cell does not exist here) |
| What you end up with | An ALTA backend standing without Docker, the full record of nine autonomous cycles, output from firing all four of ALTA's Shadow fill gates by hand, and a record of ALTA's execution boundary, carried over as rules, actually filling on weltiq and then closing and stopping itself |
| What you need · cost | Code free (Apache-2.0) · Model measured here at $0.7362 ($0.05–0.12 per cycle) · Market data paid sources are optional, but quality drops sharply without them · weltiq only if you follow section 5 |
| Time · difficulty | The install itself is 12 seconds; clearing the four blockers took about 20 minutes. Difficulty high — you need to be able to stand up PostgreSQL by hand to go without Docker |
| Relationship to weltiq | It connects. But not by filling an empty cell — by pulling out one that already exists, since ALTA already has an execution layer (Shadow) |
| Test environment · date | macOS 26.6.2 · arm64 · Node 26.8.1 · Python 3.12.13 · PostgreSQL 16 + pgvector 0.8.0 · Redis · 2026-09-21 |
| Disclosure | weltiq pays me to write this series. Everything said about the tool comes from runs I did myself |
Every order here is simulated. On the ALTA side I used only the internal Shadow and never enabled a broker connector. On the weltiq side the real-money order path is closed entirely (
live_orders: false). The tickers are examples used to verify behaviour, not trading opinions.
1. What this is
kyky2347/ALTA — Autonomous LLM Trading Asterism. The repository calls itself a "research-only multi-agent trading platform". It imitates a fictional research firm rather than a trading bot — a structure, written into code, in which a thesis only earns capital after surviving rebuttal, cost and time. Three blocks — a React console, a Node gateway (execution control, credentials, tool boundaries) and a Python research service (scheduler, lifecycle, PostgreSQL) — plus an agent harness taken from OpenAI Codex at a pinned version and modified.
| Item | Value | How it was checked |
|---|---|---|
| URL · license | github.com/kyky2347/ALTA · Apache-2.0 |
GitHub API |
| Stars | ★621 · 77 forks · 4 open issues (observed 2026-09-21) | GitHub API |
| Created / last push | 2026-08-27 / 2026-09-13 | GitHub API |
| Size | Python 135 files, 38,001 lines (tests excluded) · Node 46 files, 12,715 lines · 20 migrations | measured |
| Clone | 4.02 s · 92 MB | measured |
| Requirements | Node 22+ · Python 3.12 · uv · PostgreSQL (pgvector) · Redis · (optional) Rust |
README |
"Only two modes" is the key to understanding this tool. In Shadow, ALTA manages fills, costs, positions and exits inside itself and no broker order exists. In Broker API, only a plan that has passed audit goes to an explicitly chosen account. Whether that account is paper or live is a property of the account, not a third mode.
ALTA console, Broker connections — all six connectors Not configured
The list on the left is the six connectors. In this article all six stayed Not configured to the end. In the Alpaca form on the right, note that Paper account and Exact account ID are separate fields, and the sentence beneath them sums up this product's attitude — "Saving does not authorize trading. Verification only reads the selected account."**
There are six connectors (Tiger, Alpaca, IBKR, Futu, Longport, Schwab), but the repository nails it down first — "six implementations do not mean six approved live accounts." Longbridge and Schwab are blocked from approval outright and the rest are conditional paths. This article never touched that boundary once. Positionally ALTA is a judging tool, but unusually it already has its own execution layer (Shadow), so where weltiq fits is not "an empty cell" but "pulling out a cell that already exists" (section 5).
2. Install — the path that got all the way there without Docker
⚠ The single most important line in this part
This machine has no Docker. And I went all the way through without installing it. What follows is exactly the path that worked, with none of the four blockers hidden.
Doing what you are told gets you this far.
git clone --depth 1 https://github.com/kyky2347/ALTA.git && cd ALTA
./alta env setup --dev
Installed Python 3.12.13 in 2.11s · Prepared 30 packages in 6.03s · ALTA Python runtime ready
ALTA v3.5: Docker is missing; install and start OrbStack (recommended) or Docker Desktop
./alta env setup --dev 11.677 total
First install attempt — the Python runtime finishes in 11.7 s and the last line stops it
The Python runtime install finished in 11.677 seconds. Look at the last line — Docker is missing. That is the first wall.
Blocker ① — standing up PostgreSQL and Redis without Docker
./alta env up has exactly one branch, docker compose. But what compose.yaml wants is just two things — PostgreSQL 16 with pgvector and a password-protected Redis. The connection strings are built by alta-src/environment-config.mjs, which combines the ports and accounts in .alta/services.env with the passwords in .alta/secrets/ and injects them into child processes. In other words, if servers already listen at those addresses, Docker is not needed.
# change the ports to 9062/9063 (defaults 55432/56379)
sed -i '' 's/"55432"/"9062"/; s/"56379"/"9063"/' .alta/services.env
export LC_ALL=C # without this, postmaster dies on macOS (below)
initdb -D services/pgdata -U alta --auth=scram-sha-256 \
--pwfile=.alta/secrets/postgres_password -E UTF8 --locale=C
pg_ctl -D services/pgdata -o "-p 9062 -c listen_addresses=127.0.0.1 -c timezone=UTC" start
psql -h 127.0.0.1 -p 9062 -U alta -d postgres -c "CREATE DATABASE alta OWNER alta;"
psql -h 127.0.0.1 -p 9062 -U alta -d alta -c "CREATE EXTENSION IF NOT EXISTS vector;"
redis-server services/redis.conf # only requirepass has to match (port 9063)
CREATE DATABASE, CREATE EXTENSION, vector 0.8.0 and PONG mean it is up, and ./alta env python -m alta_asterism migrate upgrade then returns {"revision":"b20_0018"} in 0.70 seconds.
Local PostgreSQL 16 and Redis on 9062/9063 instead of Docker, through to a passing migration
Read it top to bottom — PostgreSQL starting → the vector 0.8.0 extension confirmed → Redis PONG → migration b20_0018 → the replay hash → soak PASS. Those five lines mean the ALTA backend is alive without Docker.
Forget
LC_ALL=Cand it dies withFATAL: postmaster became multithreaded during startup. That is exactly how it died the first time. It is not an ALTA problem but a known macOS + Homebrew PostgreSQL trap.
What the workaround costs: the container resource limits the compose file sets (mem_limit 1g, cpus 2, pids_limit, no-new-privileges) are not applied. On a single-user development machine that is fine; if you intend to run it resident, those limits mean something.
Blocker ② — the agent harness cannot be built
ALTA uses the pinned Codex harness in vendor/openai-codex/. The build command is V8_FROM_SOURCE=1 ./alta setup, but rust-build.mjs never downloads V8 artifacts — "Verified local V8 artifacts are required… The build never downloads them." As security design that is right, but unless you supply the artifacts yourself the build never even starts.
Workaround. Use what ALTA has already downloaded as a Python dependency.
ls .alta/python/venv/lib/python3.12/site-packages/codex_cli_bin/bin
# codex (260MB) codex-code-mode-host (46MB)
cp .alta/python/venv/.../codex_cli_bin/bin/codex .alta/bin/codex
cp .alta/python/venv/.../codex_cli_bin/bin/codex-code-mode-host .alta/bin/
It is openai-codex-cli-bin 0.144.4, and ./alta doctor recognizes it straight away.
What this one costs — and it is the biggest. This is not the harness ALTA modified. CHANGES.md honestly lists the 13 files they changed, and they include app-server/src/lib.rs and core/src/tools/handlers/multi_agents_v2.rs. The same document states that authentication, sandbox network flags, approval semantics and the wire protocol were left alone, so it is compatible at the protocol level, but the cross-provider collaboration aliases and the ALTA identity marker are missing. Everything this article ran, ran on top of that difference.
Blocker ③ — that binary rejects ALTA's configuration
$ ./alta kimi app-server
Error: error loading default config after config error:
model_providers.deepseek: provider name must not be empty
The message is one step removed. The real cause is the line above it: config.toml:8:1: invalid type: integer 4, expected struct AgentRoleToml. 0.144.4 reads the single line [agents] max_concurrent_threads_per_session = 4 differently, the whole config becomes invalid and falls back to defaults, so only the values overridden by -c model_providers.<x>.base_url=… survive and name ends up empty. For the same reason model_catalog_json was rejected too.
Workaround. I removed two lines on the generating side (evidence 19-alta-patches.diff).
- "[agents]",
- `max_concurrent_threads_per_session = ${agentThreads}`,
+ `# [agents] max_concurrent_threads_per_session = ${agentThreads}`
- "-c",
- `model_catalog_json=${toml(state.catalogs.combinedFile)}`,
+ // codex-cli 0.144.4 rejects model_catalog_json
What it costs: the per-session concurrent-thread limit (4) and the model catalog (per-model metadata such as context window and tool mode). Without the catalog the harness assumes default model properties.
Blocker ④ — no DeepSeek, Grok or Kimi keys
ALTA's default casting is heterogeneous models — scouting on DeepSeek V4 Flash, assessment DeepSeek Pro against Grok 4.6, arbitration Kimi K3, auditing Grok 4.6, and opposing roles are required to use different models. I had none of the three keys, but alta-src/providers.mjs leaves a door open.
const BASE_OVERRIDES = { deepseek: "ALTA_DEEPSEEK_BASE_URL",
xai: "ALTA_XAI_BASE_URL", kimi: "ALTA_KIMI_BASE_URL" };
The Kimi family speaks OpenAI-compatible chat/completions, so it runs on OpenRouter as is. But ALTA checks the model-name prefix (a kimi provider must start with kimi-) and OpenRouter's name is moonshotai/kimi-k2.5, so the slash trips it. I therefore put a thin shim on 127.0.0.1:9064 (95 lines, evidence 22-openrouter_shim.py) — /models returns the names with moonshotai/ stripped, and /chat/completions puts it back before forwarding. The shim passes ALTA's Authorization header straight through, so it never stores a key.
export ALTA_KIMI_BASE_URL=http://127.0.0.1:9064
export ALTA_ALLOW_INSECURE_LOOPBACK=1 # allow loopback HTTP (a switch ALTA provides)
export MOONSHOT_API_KEY=<OpenRouter key>
One more thing had to be touched. ALTA passes child agents only the environment variables on an allowlist (agent_launch.py), and ALTA_KIMI_BASE_URL was not on it, so I added two lines. After that ./alta models --refresh listed kimi-k2.5, kimi-k2-thinking and kimi-k2.6 (all context=262144).
What it costs: the heterogeneous casting is gone. Opposing roles still differ from each other, but they are all Kimi, so the diversity ALTA describes is shallower in my run. Do not read the judgement quality in this article as the quality of ALTA's default configuration.
Measurements to this point
The commands themselves are quick — clone 4.02 s (92 MB) · env setup 11.68 s · initdb 0.37 s · migrate 0.70 s · pnpm install 3.84 s (484 packages). Clearing the four blockers took about 20 minutes (from the env setup failure at 16:46 to the first completed cycle at 17:06). Disk: .alta/ 1.5 GB · node_modules 238 MB · vendor 69 MB, 1.9 GB for the whole repository. Memory during one cycle is the Python service at 77 MB plus four Node gateways at 83 MB each plus four codex processes, and the heaviest moment is the four scouts running at once.
3. First result — start with the path that runs without keys
ALTA provides a path that runs with no keys at all — no model calls, no external data, just bundled fixtures driving the entire lifecycle. If you have just installed it, start here.
$ demo {"status":"MVP_RUNNING","replayed":false, all four scouts "succeeded",
"replay_hash":"a60c6c36dd53dff2ba41b6950f4f05e79eac76d5a08e00a42aca1d067ad2527d"} 0.662s
$ replay {"replayed":true, "replay_hash":"a60c6c36dd…7ad2527d"} 0.42s
$ soak {"status":"PASS","cycles":14,"failures":[],"manual_database_repairs":0,
"session":"2026-08-24T13:30:00Z ~ 20:00:00Z"} 4.123s
That is what success looks like. Two things to look at — the replay_hash of demo and replay do not differ by a single character (same input, same lifecycle: this is the concrete form of the reproducibility ALTA sells), and soak clears a whole US session in fourteen 30-minute cycles with zero manual database repairs. The database fills with evidence 15 · run 10 · assessment 6 · opportunity 3 · expression 1 · shadow_position 1 · ops.event 36. Without a single model call or internet request, the whole lap runs: evidence → scouting → thesis → assessment → ranking → expression → Shadow entry → monitoring → exit.
I ran the contract checks too — ./alta test 306 pass / 0 fail (3.2 s), dashboard 44 pass (0.12 s), broker-python 84 pass (12.0 s), capital-python 38 pass (7.4 s). That is 472 of the 916 the repository has on record; the rest (the research integration suite) share a database with the running cycles, so I did not run them — unverified.
How to bring up the console
ALTA's face is a web console, not a terminal. ① The command is ./alta dashboard --host 127.0.0.1 --port 9070 --no-open (the same as npm run dashboard; default port 8877). ② The startup log prints endpoint: and open manually: …/open/<one-time token>, and you have to go in through /open/<token> — the bare address gives you a locked screen. Changing --host still will not open it beyond loopback. ③ There is no signup and no login: a one-time bootstrap token is the authentication, so the link works once.
Second visit — the console locks
"This console needs its secure launch URL." The bootstrap token is used once. To get back in, run ./alta dashboard again and take the new link. Locking a local-only console like this is the careful end of the spectrum.
The console, research service and Shadow are all free, and the only thing that costs money is model calls. The paid data sources (Massive, Finlight, Finnhub) are optional, but without them research quality drops sharply (section 4).
ALTA operator console, first screen — the left menu is this product's table of contents
On first open the Opportunity workspace is empty with a single Start research runtime button in the middle. Look at the bottom left — Research only / Shadow environment / Capital disabled is always attached. And RUNTIME STOPPED at the top: the console is a cockpit, not a scheduler. Values only appear once the backend is running.
The order to click is API connections (enter keys) → Agents (per-role models; opposing roles need different ones) → Start research runtime → Opportunities detail → Stop. Stop clears research and brings the database and caches down, but it does not liquidate broker holdings. And the third step is where I got stuck — Start research runtime also calls docker compose.
The Agents screen, locked while waiting for the runtime to be ready
Even inside Specialized minds there is no content — "The research runtime is recovering." The status above reads RUNTIME RECOVERING and the button is locked as Restoring readiness…. What the console watches is the management service registered with launchd, and that service also goes through docker compose. So I brought the backend up by hand as in section 2 and checked the rest from the terminal and the database. That is why the console screens in this part are empty — written down rather than hidden.
The backend can be started separately — ALTA_SERVICE_PORT=9071 ./alta env python -m alta_asterism opportunityd --host 127.0.0.1. It is read-only and has no order endpoint at all (/health/ready → {"ready":true}, /api/v1/system/summary → persisted object counts, /api/v1/stream → SSE). It came up fine, but the console still refuses to accept it as its backend — the console checks the pid and supervisor state of the launchd-registered management service, which a hand-started process cannot satisfy. I did not run ./alta service install, because that path goes through Docker too.
Remaining places it gets stuck
| Symptom | Cause | Workaround |
|---|---|---|
| The console says "needs its secure launch URL" | the bootstrap token has already been used | run ./alta dashboard again and take the new link |
./alta doctor --live kimi reports FAILED |
the probe expects a function call and the model spent only reasoning tokens | a failed probe and actual behaviour are separate things. It did work in section 4 |
| Confusion about which port is what | this article changed every default | 9062 PG (default 55432) · 9063 Redis (56379) · 9070 console (8877) · 9071 research API (8765) · 9064 the shim. The first two live in .alta/services.env, the console in --port, the service in ALTA_SERVICE_PORT |
4. Getting the most out of it — testing whether "Wait is also an answer" is real
ALTA's README says "a healthy service may be waiting for the next cycle, and Wait may be the correct outcome." It is uncommon for a trading tool to write down that doing nothing is an answer, so I measured whether it is true. I put sixteen US large caps in the universe and ran an autonomous cycle in Shadow mode.
wake.created universe=[SPY QQQ IWM DIA AAPL MSFT NVDA AMZN GOOGL META TSLA AVGO JPM XOM LLY UNH]
source.posture massive_transport disabled · finlight_transport disabled
durable_database degraded (no_durable_evidence_scout_search_allowed)
mvp.stage.completed scouts / foundry
mvp.stage.skipped private_assessment_debate · ranking · expression · shadow_open
(all no_opportunity_survived)
mvp.pipeline.completed status=MVP_IDLE expression=null
Event flow of one autonomous cycle — scouts and foundry complete, the five stages after them skipped
The three source.posture lines at the top are this cycle's premise — both paid sources disabled, database evidence degraded. Below them: scouts and foundry are completed, and the next five stages are all skipped · no_opportunity_survived. The last line, expression=null, is the conclusion.
The cycle ran to the end and bought nothing. The reason survives in the agents' own words — here is the raw output of the scouts that succeeded.
"TSLA has zero SEC filings in the signal window (2026-09-17 to 09-21). MSFT has only a routine Form 4 insider transaction, with no sign of a material non-routine event, earnings revision or change in operating status." —
change_event_scout"Search calls returned only irrelevant results (Taiwan tourism, Microsoft customer support). I could not find an auditable expectation gap for TSLA, NVDA or QQQ." —
expectation_gap_scout"The market data retrieved shows no meaningful dislocation. MSFT traded in a narrow $487–$505 range on normal volume, and TSLA rose steadily from $356 to $373 on increasing volume, consistent with momentum." —
market_dislocation_scout
The three scouts' verdicts as stored in the database — all no_op, with the reason and the tool-call count kept alongside
Under kind: no_op, reason is the sentence the agent wrote, and tools at the bottom is how many tools it actually called (2, 3 and 4). ALTA rejects the research outright if that number is 0. Lines like Finnhub unavailable and Japanese dictionary entries are what you actually get running without paid sources.
This is where the tool belongs. If the evidence is thin it says so and stops. The command is one line (autonomous --once), a cycle takes 2–5 minutes and the four scouts run concurrently. I ran nine cycles, and all three that reached the end bought nothing, with the reasons kept in structured form. But without paid data the evidence does not accumulate — the quotes above are the result.
ALTA console, API connections — the state of model, market-data and news credentials
1 CONFIGURED / 3 READY / 0 ATTENTION. In the list on the left, only Kimi / Moonshot carries API verified with an ENV badge, while DeepSeek, xAI, Massive, Finnhub and Finlight all read Credential required. This screen is the cause of section 4's results. Note the field description on the right as well — "write-only: secrets are not returned to the browser."
Changing the model split the results
The first cycle failed outright. The four scouts produced answers without calling a single tool (failed active_research_required, 4/4). ALTA rejects that — research written without a single search is not research. And it does not stop at rejecting: it returns a structured account of what was wrong and retries up to three times (scout_feedback.py). All three retries called no tools, so it failed. Switching the model from kimi-k2.5 to kimi-k2-thinking made finish_reason start showing tool_calls in the shim log, and the scouts passed from then on.
| Cycle | Scout model | Result | Scouts passed |
|---|---|---|---|
…-165229 · …-165327 |
kimi-k2.5 |
failed app_server_error |
0/4 · 0/4 |
…-165646 |
〃 | failed active_research_required |
0/4 |
…-170650 |
kimi-k2-thinking |
completed · no_opportunity_survived |
2/4 |
…-170937 |
〃 | completed · no_opportunity_survived |
1/4 |
…-171153 |
〃 + 16-name universe · effort high | failed | 0/4 |
…-171452 |
〃 | completed · no_opportunity_survived |
2/4 |
…-171603 · …-172814 |
〃 | failed (the latter a retry after recovering an interrupted cycle) | 0/4 · 0/4 |
The first two (app_server_error) were the environment-variable problem fixed in section 2 ④ — variables outside the allowlist were not passed on, so the shim address never arrived, and those two runs are why I added two lines to agent_launch.py. Of the remaining seven, three reached the end and all three bought nothing, and four failed during scouting. Same prompt, same tools, and the scout pass rate wanders between 0/4 and 2/4. That is less an ALTA defect than the price of not being able to use the default casting and substituting models, but the value for a reader is this: this pipeline hangs entirely on whether the model diligently calls its tools.
ALTA console, Agent models — assigning a model per role
You pick a model per role. Thesis assessment and Independent challenge must differ for the form to save (here kimi-k2-thinking against kimi-k2.6). The notice at the top matters — "stop the research runtime before changing models. Runs that have already executed keep their own model record." Change models later and the provenance of past records does not blur.**
I fired the stop conditions and the reconnect too
① ALTA's Shadow fill gates. I called the fill function evaluate_shadow_fill directly, feeding the same AAPL quote with only the timestamps changed.
normal (quote after the decision, fresh) → filled conservative_quote_fill @339.159495
quote arriving inside the 0.5 s frozen latency → no_fill quote_not_after_frozen_latency
quote earlier than the decision time → no_fill quote_not_forward_of_intent
quote 89 s old (limit 60 s) → no_fill price_stale
limit below the quote (not marketable) → no_fill guarded_limit_not_market
All four fired. The first two are the heart of the design — ALTA freezes the decision time (committed_at) and fills only on quotes that arrived at least 0.5 seconds after it, structurally preventing a fill against a quote earlier than the decision. Few backtesters put that in a code contract.
② But there is no price stop. The Shadow exit judgement (monitor_position) has only five reasons — thesis invalidation (falsifier_triggered), time exit, a better opportunity, a missing quote, and a stale price (60 s). There is no cell equivalent to "cut it at −2%." Thesis invalidation is a boolean an agent turns on, and the natural-language invalidation condition is a sentence, so somebody has to be awake to read it again. The broker-path documentation says the same thing in its own words — "software-managed exits, not broker protective orders. Downtime is not continuous protection."
③ The time limit on the weltiq side. I pulled the end time of the strategy container created in section 5 forward by 90 seconds.
limits_changed until 18:20:58Z → 17:34:16Z direction=tighten
autostop reason=expired position_kept=false cumulative_pnl=-0.5327405
When the deadline came it liquidated the position and stopped by itself. Bought at 339.043881, sold at 338.5111405, round trip −0.5327405 USD. Neither a person nor ALTA pressed anything.
④ Disconnect and reconnect. I killed ALTA's read-only service with SIGKILL, brought it back and asked the same question: run 186 · evidence 57 · candidate 45 · opportunity 45 · assessment 90 · expression 15 · shadow_position 15 · event 649 · run_artifact 161 · job 24 — all 18 counters unchanged. ALTA keeps state in PostgreSQL rather than in its own process, so there is nothing to recover. It is the same property as Alpaca in part 1, for a different reason — Alpaca's state lives on the broker's server, ALTA's on the database on my machine. But this is where the weltiq story starts. While it is dead, nothing is protecting the position — every exit is a software judgement, so when the process dies the judging stops. During that time the weltiq side kept running with a protective stop already registered as an order. What survives is the record, not the protection.
What I did not run
| Why | |
|---|---|
| Looking at one global ticker | it is not a quote viewer. Scouts fetch data, but there is no interface that hands out candles — not applicable |
| Korean tickers | this tool cannot. Both the universe and the connectors are US (plus Hong Kong via Futu) |
| Screening many names down to a shortlist | section 4 is exactly that — sixteen names narrowed to zero |
| Re-measuring with different fees | there was only one fill, so comparing rates is meaningless. Not run |
| The six broker connectors | never enabled once. I did not enter the account-verification or approval path |
| Paid sources (Massive, Finlight, Finnhub) | no keys. Unverified — most of this part's results sit on top of that absence |
| The research Python integration suite | it shares a database with the autonomous cycles. Not run |
5. Moving it to weltiq — pulling out a cell that already exists
ALTA's execution layer is not empty — Shadow already occupies it, so the question is not "how do I fill the blank" but "why, and how far, do I pull out something that is already there".
5-1. The order of the move, and where each field came from
ALTA's conclusion is a record called an Expression, not an order. Moving it means rewriting that record as a rules document, in four steps — ① POST /v1/strategy-rules for the rules document ② POST /v1/ai/thesis for the rationale ③ POST /v1/containers for a strategy container with stop conditions ④ unattended execution. Steps ② and ③ cannot be skipped, and leaving them out fails like this.
without stop conditions: {"detail":[{"type":"missing","loc":["body","stop_max_loss"],"msg":"Field required"},
{"type":"missing","loc":["body","stop_after_hours"],"msg":"Field required"}]}
without a rationale: {"detail":{"code":"thesis_required","questions":[
"Write in one sentence, in your own words, why this strategy makes money.",
"Under what market conditions does this logic break?",
"Specify the invalidation condition as a number (a price level or a point in time)."]}}
Those two gates are the fence around autonomy. However many times ALTA judges for itself, the door into resident execution needs a limit a human wrote and a rationale a human wrote. Here is where each field came from, and where the author starts.
| Field on the weltiq side | Value | Source |
|---|---|---|
| Direction | long |
ALTA ExpressionProposal — side only allows "long" |
| Whole shares · regular-hours gate · one entry | 1-share units · clock('regular') · execution('daily_entries') < 1 |
ALTA's execution-boundary doc: "whole shares", "regular-hours boundary", "One active plan per account" |
| Time exit | stop_after_hours |
ALTA PositionThesis.time_exit_at |
| Quantity | 1 (at the time) | The Expression record ALTA actually produced said 10 → 5-3. Since the 2026-09-23 fix, the 10 goes through as it is |
| Price stop | −1.5% from average | A cell ALTA does not have. It exists here because weltiq requires it |
| Ticker | AAPL | ALTA did not pick one. All three completed cycles said Wait, so the author supplied an example |
The last two rows are this part's honesty. The ticker bought was not ALTA's choice.
5-2. The best pairing — ALTA judges, weltiq fills and protects
I put the same AAPL, at close to the same moment, through both fill models. On the ALTA side I imported evaluate_shadow_fill directly (nothing was copied across), and the weltiq side is a fill it actually produced.
ALTA Shadow quote bid 338.94 / ask 338.99
fill 339.159495 = ask × (1 + 5bp) slippage exactly 5.0000bp
commission 0.033916 = notional × 1bp (minimum $0.01)
weltiq expected 338.91 → fill 339.043881 slippage 3.95bp (measured)
commission 0E-8 USD (us-equity paper is zero)
protective stop 333.7918750 registered automatically at the same instant
(stop_market, reduce_only)
The two numbers look close but they were made in different ways. ALTA's 5.00 bp is a policy constant; weltiq's 3.95 bp is the result of walking down the book. Where the difference shows is quantity.
qty 1 → filled 339.159495 commission 0.033916 slippage 5.0000bp
qty 10000 → filled 339.159495 commission 339.16 slippage 5.0000bp
(actual ask-side size at that moment: 80 shares)
There are 80 shares on the offer and it fills 10,000 at the same price — 125 times the size. ALTA's Shadow does not look at resting size. That is not an accusation but the range of the instrument — the purpose is to measure whether a thesis survives costs, not to measure liquidity. But use this instrument to validate thin names in large size and the results will be structurally flattering.
| ALTA Shadow | weltiq | |
|---|---|---|
| Fill basis | quote × a fixed 5 bp | walking 20 levels of the book |
| Size sensitivity | none (1 share = 10,000 shares) | present |
| Commission | 1 bp, minimum $0.01 (constant) | the real fee schedule (us-equity paper is zero) |
| Lookahead prevention | yes — only quotes 0.5 s after the decision | closed bars |
| Price stop | none | required — without one the container does not start |
| Thesis invalidation | a boolean an agent turns on | — (it cannot read sentences) |
| While switched off | the judging stops | the registered protective order remains |
What this combination does not give you. weltiq cannot read ALTA's natural-language invalidation conditions. "Exit if a regulatory announcement breaks the thesis" is a sentence, and the rules grammar has no cell for it, so ALTA has to wake up and judge. It is a pairing that divides roles, not one where either side replaces the other.
5-3. One lap of the loop — the first cell stayed empty
Stated plainly first: I could not fill the first cell of this loop with a live ALTA judgement. All three of the nine cycles that reached the end said no_opportunity_survived, so the sentence "weltiq filled what ALTA said to buy" does not appear in this article — unverified. What I did get through is whether the plumbing runs end to end.
① Set the fence — stop at 60 USD of cumulative loss, auto-close after one hour, protective stop 1.5%. ② Submit the rules and rationale and create the container — rules document fingerprint c659c651b5d7…, rationale {"ok":true}, container status=running. ③ And the entry was blocked.
{"light":"yellow","code":"entry_blocked","reason":"DENY",
"text":{"ko":"진입이 한도·수량 규칙으로 막힘"}}
The cause is on the weltiq side. A rules document uploaded with an API key cannot carry policy limits (inserting a _q key directly returns 422 forged_provenance — still true today), so at the time the order quantity ceiling stood at its default of 1. The Expression record from ALTA carried a quantity of 10, so the first attempt hit it immediately.
Afterwards (2026-09-23): fixed. Even without
_q, thesizing.qtydeclared by the rules document is now read as the order ceiling — upload the same document today and the 10 goes through as it is. The "brought it down from 10 to 1 and resubmitted" below is what actually happened that day, so it stays.
④ Fix it on that evidence — I lowered the quantity from 10 to 1 and rebuilt the document (fingerprint f182c5703f8d…), and ⑤ resubmitted, and it filled.
order buy market 1 signal=pine_entry:A → filled
order sell stop_market 1 stop=333.7918750 reduce_only=true → acked
fill AAPL 1 @ 339.043881 fee 0E-8 USD quote_source=alpaca-iex liquidity=taker
expected 338.91 → slippage 3.95bp
weltiq strategy container detail — ALTA's execution boundary, carried over as rules, actually filling
The line under the title is the transferred rule — (regular hours AND (position.size = 0)) AND (execution(daily_entries) < 1) → enter long A. Look at ORDERS: at the same instant as the buy fill, the protective stop (stop_market) is sitting there acked. That order remains even with ALTA switched off. Auto-stop −60 · 03:20 AM on the right is the fence set in ①.
⑥ And I watched the fence work — pulling the deadline forward in section 4 made it liquidate and stop by itself (round trip −0.5327405 USD). The loop closed. But one cell in it was substituted.
| Cell in the loop | Who filled it |
|---|---|
| Analysis | ALTA — nine cycles; all three that finished concluded Wait |
| What to buy | not ALTA — the author supplied AAPL as an example |
| How to execute | ALTA — regular hours, whole shares, one plan per account, time exit |
| Order and fill | weltiq — 1 share @ 339.043881, protective stop registered simultaneously |
| Reading the result and fixing | weltiq's records — quantity 10 refused → resubmitted at 1 |
| The unattended stretch | weltiq — kill ALTA and the stop order still stands |
The same document passing through backtest and paper as one body held here as well. The live segment, however, is still not open (live_orders: false).
6. Having used it
What was good
- "Doing nothing" is a first-class result. All three of the nine cycles that reached the end said
Wait, and each time why it did nothing survived in an agent's sentence. It is uncommon for a trading tool to record empty hands as a success. - It rejects research that did no research. Zero tool calls means rejection, plus a structured account of what was wrong and up to three retries.
- Lookahead by contract, reproducibility by hash. It freezes the decision time and fills only on quotes 0.5 seconds later, the
demoandreplayhashes were identical, and a 14-cycle intraday soak passed with zero manual database repairs. - It writes its own boundaries down first. "Six connectors are not six approved live accounts", "software-managed exits, not broker protective orders", "downtime is not continuous protection" — all sentences from its own documents. Credential isolation is real too, as proved by my shim address being blocked (section 2 ④).
What was disappointing
- Installation is heavy and Docker is effectively mandatory. Both
env upandStart research runtimehave the singledocker composebranch, so even after working around it by hand the console never escapedRUNTIME STOPPED. Building the harness also requires sourcing V8 artifacts yourself. - Paid data sources are effectively a prerequisite. Empty, the scouts fall back to free public search and genuinely return "Taiwan tourism" and "Japanese dictionary" results. The README presents them as optional, but the quality of the outcome is decided there.
- There is no price stop. The exit reasons are thesis invalidation, time, a better opportunity and stale prices, and thesis invalidation needs an agent awake to judge it.
- Shadow fills ignore resting size. It fills 10,000 shares at the same price against 80 on the offer.
- The scout pass rate depends heavily on the model. With substitute models it wandered between 0/4 and 2/4. How the default casting behaves is unverified. And for a 621-star project the repository is large (1.9 GB).
Cost
| Repository, console, Shadow | Free |
| Model calls (138 in this article, Moonshot Kimi via OpenRouter) | $0.7362 · roughly $0.05–0.12 per cycle |
| Paid data sources | Not used. Had I used them, that would be the largest cost |
Total usage on the same key rose by $1.5707 over the same window, but other work was running on that key, so the amount attributable to this article is the $0.7362 the shim recorded call by call. Both are written down.
7. Coming up next
Part 12 is TauricResearch/TradingAgents. Where ALTA makes a single agent's judgement auditable, this one makes several agents argue with each other — analysts split the reading, a risk team attacks the conclusion, and a trader writes the final proposal. One thing I am particularly curious about: it is the only tool I have run that has ever answered "do nothing." Whether that is its character or the luck of that day — I will run it many times and count. Part 9 taught me not to call a tool cautious on the strength of one cautious answer.
weltiq setup — 3 lines 1. Sign up: {{CTA_URL}} 2. My → Connections → issue an API key 3.
claude mcp add weltiq --header "Authorization: Bearer <key>" https://weltiq.ai/mcpIf the stop has to still be there while ALTA is switched off, that is when to look.
Every command, output and number in this article came from runs actually performed on 2026-09-21. No real money moved in any order; all of it is simulated. None of it is a trading opinion. The trial and error, raw logs and defect list not written up here are in the run log.