Spaces:
Running
title: Stocker - OpenEnv
colorFrom: blue
colorTo: green
sdk: docker
pinned: false
app_port: 7860
tags:
- openenv
- multi-agent
- rl
- finance
- grpo
Stocker β Multi-Agent Council RL for Stock Trading
π€ Environment Space Β· π― Training Space Β· π Training Notebook Β· π Blog Β· π» GitHub
Problem: LLMs can reason about markets from multiple angles simultaneously β charts, news, macro, technicals, sentiment. Can we train a moderator LLM to synthesize seven specialist perspectives into profitable trades, purely via RL?
Approach: Seven frozen specialist LLMs vote each step; a trainable moderator merges their votes. The moderator is fine-tuned with GRPO (Group Relative Policy Optimization) using the environment's own reward as the training signal. No human labels, no supervised data β reward comes from the env.
Results: See training/runs/RESULTS.md for the pre-training vs post-training comparison table and plots.
A long-term stock-trading RL environment built on OpenEnv. Every step,
seven specialist analyst agents examine the market through different lenses
and emit a vote; a moderator LLM merges the seven votes into a single
(side, quantity) trade. The moderator is fine-tuned with GRPO (TRL)
using the environment's own reward as the training signal.
Base model: google/gemma-4-E4B-it β
multimodal (text + image), 4-billion parameters, Apache 2.0.
Council architecture
OpenEnv interface (single-agent contract)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POST /reset β MarketObservation(ticker, date, β¦) β
β POST /step β TradeAction(side, quantity) β
βββββββββββββββββββββββββ²ββββββββββββββββββββββββββββββ
β inference.py orchestrates:
ββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββ
β per step β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 7 specialists run in PARALLEL β β
β β ChartPattern (vision: 60-day candlestick PNG) β β
β β SeasonalTrend (long-term + cycle context) β β
β β Indicator (RSI / MACD / SMA / BB / ATR) β β
β β News (curated headlines, ~7-day window) β β
β β ForumSentiment (Reddit excerpts) β β
β β PeerCommodity (peer stocks + gold/oil correlation) β β
β β Geopolitics (CPI / FOMC / sanctions / fiscal) β β
β β β SpecialistVote(signal β [-1,1], confidence, why) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β Moderator (Gemma 4 E4B IT + LoRA) β
β sees 7 votes β outputs TradeAction + rationale β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
env.step(action) β reward
(performance vs ideal trajectory β inflation drag)
Key invariant: the OpenEnv contract stays single-agent. Council
orchestration lives in inference.py + app/council/, not inside step().
openenv validate passes and graders can re-run inference exactly.
Methodology
Dataset
Three deterministic episodes built from real OHLCV data (yfinance):
| Task | Ticker | Window | Regime |
|---|---|---|---|
task_easy |
AAPL | AugβSep 2023 | Steady uptrend into iPhone 15 launch |
task_medium |
INTC | JanβFeb 2024 | Choppy / sideways post-earnings |
task_hard |
META | SepβOct 2022 | Drawdown then snap-back after Q3 print |
Each episode is 41β43 trading days. All OHLCV, indicators, curated
headlines, forum excerpts, macro events, and 768Γ768 candlestick PNGs are
bundled in data/ and versioned in git (6.2 MB).
An optional corpus (15 tickers Γ 20 years, ~3 GB) can be built with
python scripts/build_corpus.py and exposes 4 000+ additional episodes via
corpus_* task IDs.
Specialists (frozen)
Each specialist has a fixed system prompt and calls the same LLM (Gemma 4
E4B IT in 4-bit, or any OpenAI-compatible endpoint). Their outputs are
cached by (role, ticker, date) after the first call, so they are
never re-rolled during GRPO β only the moderator is trained.
| Specialist | Input | Dimension |
|---|---|---|
ChartPattern |
candlestick PNG (multimodal) | visual patterns |
SeasonalTrend |
price history + fundamentals | macro cycle |
Indicator |
RSI, MACD, SMA, BB, ATR | technical signals |
News |
curated headlines | sentiment |
ForumSentiment |
Reddit excerpts | crowd sentiment |
PeerCommodity |
peer stocks + gold/oil | correlation |
Geopolitics |
CPI, FOMC, macro events | macro risk |
Each returns {"signal": float β [-1,1], "confidence": float β [0,1], "rationale": str}.
Reward function
Per-step reward is a weighted combination of two components:
reward_step = W_PERF Γ performance_factor(gap)
+ W_INFL Γ inflation_factor
β invalid_action_penalty (0.01 if action invalid)
Performance factor β asymmetric piecewise-linear function of the gap between the pre-computed ideal PnL trajectory and the model's actual inflation-adjusted PnL at this step:
gap = ideal_pnl_pct[step] β real_pnl_pct
if gap < 0: performance_factor = 1 + min(1, |gap| / scale) # outperformance bonus
elif gap β€ scale: performance_factor = 1 β gap / scale # close to ideal
else: performance_factor = βmin(1, (gap β scale) / scale) # far behind
where scale = max(0.05, 0.5 Γ |ideal_pnl_pct_total|).
The ideal PnL trajectory is pre-computed by scripts/build_ideal_profit.py
using the optimal hindsight strategy (greedy day-by-day).
Inflation factor β penalty for the share of nominal gain eaten by inflation:
real_pnl_pct = (1 + nominal_pnl_pct) / (1 + inflation_growth) β 1
inflation_factor = real_pnl_pct β nominal_pnl_pct (β€ 0)
Trajectory bonus (added to the last step):
alpha_bonus = min(0.10, max(0, (final_portfolio / buy_and_hold_portfolio) β 1))
dd_penalty = min(0.10, max(0, max_drawdown β 0.05))
Default weights: W_PERF = 0.7, W_INFL = 0.3, annual_inflation_rate = 0.05,
transaction_cost_rate = 0.001. All are configurable via STOCKER_* env vars
(see app/config.py) and sweepable with the tune_easy_gemma4.ipynb replay grader.
All rewards are clipped to [-1.0, 1.0] at the env boundary.
Training (GRPO)
The 7 specialists are frozen. Only the moderator is fine-tuned
via trl.GRPOTrainer with a LoRA adapter (rank 16) on Gemma 4 E4B IT.
Dataset: task_easy (43 moderator prompts, each containing 7 specialist
votes and the current market state).
Settings (L4-tuned for the HF training Space):
| Parameter | Value |
|---|---|
| Base model | google/gemma-4-E4B-it (4-bit BnB, ~3 GB) |
| Inference (specialists + production) | ggml-org/gemma-4-26B-A4B-it-GGUF via HF endpoint (llama.cpp) |
| LoRA rank / alpha | 16 / 32 |
| Epochs | 3 |
num_generations |
4 |
per_device_train_batch_size |
4 |
| Learning rate | 5e-6 |
| Compute | HF Space, Nvidia L4 24 GB (~$0.80 / training run) |
Each GRPO step: sample 4 moderator completions per prompt β parse each into
TradeAction β simulate env step β compare rewards β update LoRA. Specialist
votes come from cache (no LLM calls); only the moderator is re-rolled.
Reward-weight tuning
training/tune_easy_gemma4.ipynb runs a full episode, captures per-step
reward breakdowns, then re-grades the saved trace under a sweep of weight
combinations (pure CPU, sub-second per combo). This lets us iterate on the
reward function without re-running the LLM:
swept 180 combos over (W_PERF, W_INFL, inflation_rate, transaction_cost)
best: W_PERF=1.0, W_INFL=0.0, tc=0.0 β total=+0.5725
default: β total=+0.3801
Results
Training run results are compiled automatically by
scripts/compile_results.pyafter training. Seetraining/runs/RESULTS.mdfor the latest numbers and plots.
Quick start
A. Hit the running env Space (zero-setup, recommended for graders)
The OpenEnv contract is live at the HF Space β interact with /reset, /step,
/state directly:
# Reset to task_easy
curl -X POST https://hydr473-stocker-env.hf.space/reset \
-H "Content-Type: application/json" \
-d '{"task_id": "task_easy"}'
# Submit a trade
curl -X POST https://hydr473-stocker-env.hf.space/step \
-H "Content-Type: application/json" \
-d '{"side": "buy", "quantity": 10}'
Or open https://hydr473-stocker-env.hf.space/web for the interactive React UI
(six tabs: Terminal, Council, Training, Gallery, Portfolio, Intelligence).
B. Run GRPO training (one click on the L4 GPU Space)
Open Hydr473/stocker-train and click π Launch Pipeline. The Gradio UI streams the full 6-phase run (precache β eval_pre β GRPO β eval_post β compile β upload) and uploads the final plots + LoRA adapter to Hydr473/stocker-results. Cost: ~$0.80 / run on the L4.
C. Local development (mock client, no GPU)
pip install uv
uv pip install -e ".[dev,data,eval]"
python scripts/build_dataset.py
python scripts/validate_tasks.py
# Deterministic offline smoke (no LLM calls)
python inference.py --task all --mock --no-cache
# Local server + React UI
./run.sh # http://localhost:7860/web
pytest tests/ -q
To run the council against your own endpoint, set API_BASE_URL,
MODEL_NAME, and HF_TOKEN (see .env.example).
Council vote cache
Each (role, ticker, date) triple is computed once and cached as JSON. The
env Space pulls a pre-warmed cache from the HF dataset
Hydr473/stocker-cache
on startup so judges' UI clicks are instant. To re-warm:
# 1. Compute votes via your endpoint (writes .cache/council/<role>/base/...)
python scripts/precache_endpoint.py --tasks task_easy,task_medium,task_hard
# 2. Upload to the dataset repo
python scripts/upload_cache.py # uses $STOCKER_CACHE_REPO or default
# 3. Restart the env Space β its lifespan hook downloads the new cache.
The startup hook is no-op (and never crashes) when STOCKER_CACHE_REPO is
unset, so local dev still works without HF Hub access.
Evaluation
# Baseline (no LoRA)
python -m training.eval_rollout --tasks task_easy --out training/runs/eval_pre
# After GRPO
python -m training.eval_rollout --tasks task_easy --moderator-lora moderator \
--out training/runs/eval_post
# Compile artifacts + diff table
python scripts/compile_results.py
API
| Endpoint | Method | Description |
|---|---|---|
/web |
GET | Interactive React frontend |
/health |
GET | Health check |
/meta |
GET | Environment metadata |
/reset |
POST | Reset ({"task_id": "task_easy"}) |
/step |
POST | Submit {"side": "buy", "quantity": 10} |
/state |
GET/POST | Export / restore env snapshot |
/docs |
GET | Swagger UI |
Layout
.
βββ app/
β βββ api/ # FastAPI routers
β βββ council/
β β βββ llm.py # TransformersLLMClient, OpenAILLMClient, MockLLMClient
β β βββ specialists.py # 7 role-specific prompted agents
β β βββ moderator.py # merges votes β TradeAction (+ optional LoRA)
β β βββ runner.py # ThreadPoolExecutor + on-disk cache
β βββ core/
β β βββ environment.py # StockerEnv: reset / step / state / load_snapshot
β β βββ graders.py # compute_step_reward, compute_trajectory_bonus
β β βββ tasks.py # get_task_definition + corpus integration
β βββ data/
β β βββ loader.py # parquet + chart lookups + corpus fallback
β β βββ corpus.py # optional large corpus (15 tickers Γ 20 y)
β βββ models.py # all Pydantic schemas
βββ data/
β βββ *.parquet # prices, indicators, news, peers, macro
β βββ ideal_profits/ # hindsight-optimal PnL sidecars
β βββ charts/ # 768Γ768 candlestick PNGs
β βββ sources/ # curated JSON (news, forums, macro)
βββ scripts/
β βββ build_dataset.py # yfinance β parquet + charts
β βββ build_ideal_profit.py # per-task optimal PnL trajectory
β βββ build_corpus.py # large corpus (optional, gitignored)
β βββ compile_results.py # compile training artifacts β RESULTS.md
β βββ precache_endpoint.py # standalone specialist pre-cache via endpoint
β βββ validate_tasks.py
βββ training/
β βββ train_grpo.py # GRPO trainer (CLI, --tasks filter)
β βββ train_grpo.ipynb # end-to-end Colab/Jupyter notebook
β βββ tune_easy_gemma4.ipynb # reward-weight tuning bench
β βββ eval_rollout.py # offline backtest β reward/portfolio curves
β βββ runs/ # gitignored (except .gitkeep)
βββ spaces/
β βββ train/ # Gradio app for the L4 GPU training Space
β βββ Dockerfile # CUDA + PyTorch + git clone of this repo
β βββ app.py # 6-phase pipeline: precache β eval β GRPO β upload
βββ .github/workflows/
β βββ deploy_spaces.yml # CI: auto-deploy both Spaces on push to main
βββ inference.py # council-driven OpenEnv inference loop
βββ tests/
βββ Dockerfile # multi-stage: build React UI β Python runtime
βββ pyproject.toml
Credits
- Built on OpenEnv following the submission spec.
- Real OHLCV: yfinance (Yahoo Finance public endpoints).
- Model: google/gemma-4-E4B-it (Apache 2.0).
- Curated headlines + forum excerpts:
data/sources/(drop-in replaceable).