Instructions to use poolside-laguna-hackathon/trade-pool with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use poolside-laguna-hackathon/trade-pool with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- LOOP.md +72 -0
- SPEC.md +137 -0
- baseline_eval.txt +219 -0
- configs/iter_1.toml +26 -0
- configs/laguna-stock-strategy.toml +27 -0
- reward_curve.txt +1 -0
- trade_pool/__init__.py +214 -0
- trade_pool/backtester.py +162 -0
- trade_pool/data.py +81 -0
- trade_pool/executor.py +67 -0
- trade_pool/features.py +110 -0
- trade_pool/seed_principles.py +67 -0
LOOP.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The Recursive Self-Improving Loop
|
| 2 |
+
|
| 3 |
+
How tradewatch's soft reflection (events → MEMORY.md prompt text) becomes a real
|
| 4 |
+
gradient loop on Laguna XS.2, where improvement compounds across iterations through
|
| 5 |
+
**both adapter weights and curriculum**.
|
| 6 |
+
|
| 7 |
+
## The two improvement channels
|
| 8 |
+
|
| 9 |
+
1. **Weights (parametric continuation):** each hosted RL run warm-starts from the prior
|
| 10 |
+
iteration's adapter via `checkpoint_id`. The model is never reset — discipline learned
|
| 11 |
+
in iter N carries into iter N+1. This is the thing tradewatch never had.
|
| 12 |
+
2. **Curriculum (reflection-driven):** between runs, `recursive_loop.py reflect` reads the
|
| 13 |
+
prior adapter's OOS eval and shifts the next run's **objective** (sharpe → min_drawdown
|
| 14 |
+
→ balanced) and **focus symbols** (the weakest performers). This is tradewatch's
|
| 15 |
+
`summarize_session_events` reflection — repurposed to steer RL instead of prompt notes.
|
| 16 |
+
|
| 17 |
+
## One iteration
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
┌──────────────────────────────────────────────┐
|
| 21 |
+
│ configs/rl/iter_N.toml │
|
| 22 |
+
│ model=poolside/Laguna-XS.2 │
|
| 23 |
+
│ checkpoint_id=<iter N-1 adapter> ← weights │
|
| 24 |
+
│ [[env]] objective=..., symbols=[weak...] │
|
| 25 |
+
└───────────────────┬──────────────────────────┘
|
| 26 |
+
prime train run iter_N.toml │ (FREE hosted RL, GRPO, 128 rollouts/step)
|
| 27 |
+
▼
|
| 28 |
+
LoRA adapter ──► prime deployments create
|
| 29 |
+
│ base:adapter_id, OpenAI-compatible
|
| 30 |
+
▼
|
| 31 |
+
python scripts/laguna_eval.py --model base:adapter_id --split oos_symbols
|
| 32 |
+
(writes strategy per HELD-OUT symbol, scores via rubric)
|
| 33 |
+
│ logs/eval_*.json
|
| 34 |
+
▼
|
| 35 |
+
python scripts/recursive_loop.py reflect <eval.json> --checkpoint-id <adapter>
|
| 36 |
+
(curriculum policy → objective + weak-symbol focus)
|
| 37 |
+
│
|
| 38 |
+
▼
|
| 39 |
+
configs/rl/iter_{N+1}.toml ──► loop repeats
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Note Prime enforces **max 1 concurrent run/user**, so iterations are sequential — which
|
| 43 |
+
is exactly what warm-starting requires anyway (iter N+1 needs iter N's adapter to exist).
|
| 44 |
+
|
| 45 |
+
## Curriculum policy (`_choose_objective`, inspectable & deterministic)
|
| 46 |
+
- valid≥0.8 but mean_total<0.5 → `min_drawdown` (strategies run but lose → control risk)
|
| 47 |
+
- pct_wrote_code<0.7 → `sharpe` + more steps (model still learning to code)
|
| 48 |
+
- otherwise → `balanced` (competent → broaden)
|
| 49 |
+
- always: next run focuses the 3 weakest OOS symbols, rotates `seed` for fresh task mixes,
|
| 50 |
+
lengthens to 75 steps if learning stalled (<0.5).
|
| 51 |
+
|
| 52 |
+
## Closing to tradewatch (the demo)
|
| 53 |
+
The deployed adapter is OpenAI-compatible, so tradewatch's existing `HybrieClient` runs it
|
| 54 |
+
live with one config change:
|
| 55 |
+
```
|
| 56 |
+
base_url = https://api.pinference.ai/api/v1
|
| 57 |
+
model = poolside/Laguna-XS.2:<adapter_id>
|
| 58 |
+
```
|
| 59 |
+
**Ablation money-shot:** run the adapter with MEMORY.md stripped from the prompt. If the
|
| 60 |
+
discipline holds, it's provably in the weights — the memory became the adapter.
|
| 61 |
+
|
| 62 |
+
## Run it
|
| 63 |
+
```bash
|
| 64 |
+
# bootstrap iteration 1
|
| 65 |
+
python scripts/recursive_loop.py init --env-id <you>/stock-strategy-env --model poolside/Laguna-XS.2
|
| 66 |
+
prime train run configs/rl/iter_1.toml
|
| 67 |
+
prime deployments create <adapter_id>
|
| 68 |
+
export PRIME_API_KEY=...
|
| 69 |
+
python scripts/laguna_eval.py --model poolside/Laguna-XS.2:<adapter_id> --split oos_symbols
|
| 70 |
+
python scripts/recursive_loop.py reflect logs/eval_*.json --checkpoint-id <adapter_id>
|
| 71 |
+
# -> configs/rl/iter_2.toml ready; repeat
|
| 72 |
+
```
|
SPEC.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# hack-x — Environment & Training Spec
|
| 2 |
+
|
| 3 |
+
A self-improving crypto-trading **coding agent** trained with RL on Prime Lab.
|
| 4 |
+
tradewatch's soft reflection loop (events → MEMORY.md prompt rules) becomes a real
|
| 5 |
+
gradient loop: rollout → backtest verifier → GRPO → **LoRA adapter** → deploy → live demo.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 1. The one-line thesis
|
| 10 |
+
> Today tradewatch's trading discipline lives as *prompt text* (MEMORY.md). We make it
|
| 11 |
+
> live as *adapter weights* by rewarding disciplined, profitable behavior on
|
| 12 |
+
> out-of-sample replayed market history. The proof: the trained adapter stays
|
| 13 |
+
> disciplined with MEMORY.md **removed from the prompt**.
|
| 14 |
+
|
| 15 |
+
## 1b. tradewatch as seed (pull all of it — but know its limits)
|
| 16 |
+
Mined both DB (4,875 events) and journal (708 rows). What's there:
|
| 17 |
+
- **618 `analysis_decision` rows** with 18 market features each (price_usd, liquidity_usd,
|
| 18 |
+
volume_24h/1h, buys/sells 1h+24h, buy_sell_24h, vol/liq, price_change 5m→24h, age_hours,
|
| 19 |
+
fdv, market_cap) — this IS the agent's feature schema + seed/format examples.
|
| 20 |
+
- Real verdicts + entry/target/stop + reasoning over 9 tokens (action-surface-B examples).
|
| 21 |
+
- **9 confirmed pool addresses** → guaranteed-good fetch universe seed.
|
| 22 |
+
- Trade outcomes (exits/PnL/skips) → rubric calibration.
|
| 23 |
+
- **NOT present:** any stored OHLCV time series. `ohlcv_1h` was fetched live, never persisted.
|
| 24 |
+
Every market row is a point-in-time snapshot, not a replayable tape.
|
| 25 |
+
- **Therefore:** tradewatch = seed of *decisions/features/pools* (the brains + universe);
|
| 26 |
+
GeckoTerminal fetch = the *replay price tape*. Both required; neither substitutes.
|
| 27 |
+
|
| 28 |
+
## 1c. The three loops are independent (demo ≠ batch_size)
|
| 29 |
+
- **Training loop:** batch_size=128 parallel replay-rollouts/step — a GRPO requirement only.
|
| 30 |
+
- **Demo loop:** 1 (or few) LIVE paper session in tradewatch, trained adapter via
|
| 31 |
+
base:adapter_id. As small as we want. 128 was never the demo.
|
| 32 |
+
- **Recursive loop (stretch):** reflect → checkpoint_id warm-start → retrain.
|
| 33 |
+
|
| 34 |
+
## 2. Data (replay substrate) — crypto
|
| 35 |
+
- **Source:** GeckoTerminal free OHLCV — `/networks/base/pools/{pool}/ohlcv/{tf}?aggregate=N&limit=L`
|
| 36 |
+
(already used in `scanner/base_scanner.py:843`). No key.
|
| 37 |
+
- **Universe:** start from the 9 journal tokens (NOCK, VIRTUAL, DEUS, CTR, Surplus,
|
| 38 |
+
GITLAWB, VVV, DEGEN, PITCH) + expand via trending-pool discovery to ~30–50 Base pools.
|
| 39 |
+
- **Bars & DEPTH CEILING (probed live 2026-05-30):** free OHLCV caps at **~1000 bars/pool,
|
| 40 |
+
`before_timestamp` pagination does NOT walk back further.** So:
|
| 41 |
+
- hourly (`tf=hour`): ~1000 bars ≈ **41 days**
|
| 42 |
+
- daily (`tf=day`): ~181 bars ≈ **6 months**
|
| 43 |
+
- **Implication:** shallow history → depth can't supply many decorrelated time windows.
|
| 44 |
+
**Breadth carries GRPO variance: tasks = symbol × window, universe = 30–50 pools.**
|
| 45 |
+
- **Storage:** `data/ohlcv/<symbol>.parquet` (ts, o, h, l, c, v). Reproducible, no rate limits at train time.
|
| 46 |
+
- **Splits (anti-leakage, structural):**
|
| 47 |
+
- per-symbol time split: train window | OOS window (chronological, no overlap)
|
| 48 |
+
- held-out OOS **symbols** (never seen in training) for generalization check
|
| 49 |
+
- with shallow per-symbol history, the symbol-holdout split does the heavy lifting
|
| 50 |
+
- **Crypto-hardening (because reward is gameable here):**
|
| 51 |
+
- slippage + fee model on every fill (fixed bps + size-vs-liquidity impact)
|
| 52 |
+
- min-liquidity / min-volume gate per bar (illiquid bars can't be traded)
|
| 53 |
+
- survivorship caveat noted; mitigate by including tokens that later died
|
| 54 |
+
|
| 55 |
+
## 3. Action surface — support BOTH (decide after baseline rollouts)
|
| 56 |
+
The backtester accepts a uniform `target_positions` dict; two agent framings feed it:
|
| 57 |
+
- **A. Strategy-code:** agent writes `strategy(features_window) -> target_positions`,
|
| 58 |
+
executed causally over the OOS window. Strongest "coding agent" framing.
|
| 59 |
+
- **B. Structured decisions:** agent emits per-bar JSON (verdict/entry/target/stop),
|
| 60 |
+
reusing tradewatch's schema. Closer to existing code.
|
| 61 |
+
Both reduce to the same backtest call → same rubric. We prototype both, pick via eval.
|
| 62 |
+
|
| 63 |
+
## 4. Backtester (port `agents/paper_ledger.py`)
|
| 64 |
+
- `ReplayFeed`: serves bars **only up to t** — lookahead impossible by construction
|
| 65 |
+
(the agent/strategy literally never receives `bars[t+1:]`).
|
| 66 |
+
- Engine: step bar-by-bar, apply target_positions via slippage/fee model, mark-to-market,
|
| 67 |
+
track equity, cash, exposure, per-trade R:R, drawdown.
|
| 68 |
+
- Metrics: Sharpe (primary), CAGR/return, max drawdown, equity/cash ratio, turnover, win rate.
|
| 69 |
+
- Benchmarks computed on same window: **buy-and-hold**, MA-crossover, z-score mean-reversion.
|
| 70 |
+
- Invariants (unit-tested): causality (no future leak), determinism (seed → identical run).
|
| 71 |
+
|
| 72 |
+
## 5. verifiers environment (v0 stable API)
|
| 73 |
+
Class: **`StatefulToolEnv`** (per-rollout state + stateful tools).
|
| 74 |
+
|
| 75 |
+
```python
|
| 76 |
+
def load_environment(
|
| 77 |
+
symbols: list[str] | str = "train", # universe or split name
|
| 78 |
+
split: str = "train", # train | oos | oos_symbols
|
| 79 |
+
objective: str = "sharpe", # sharpe | return | min_drawdown
|
| 80 |
+
max_turns: int = 8,
|
| 81 |
+
n_windows: int = 4, # OOS windows averaged per reward
|
| 82 |
+
seed: int = 0,
|
| 83 |
+
) -> vf.Environment: ...
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
- `setup_state(state)`: pick task (symbol(s) × window × objective), build fresh ReplayFeed + ledger.
|
| 87 |
+
- **Tools** (stateful, NOT per-rollout sandbox — global/in-proc exec to save credits):
|
| 88 |
+
- `get_features(lookback)` → indicators up to current bar (RSI, MACD, MAs, z-score,
|
| 89 |
+
BB, vol, buy/sell ratio, liquidity) — reuses scanner feature logic
|
| 90 |
+
- `run_backtest(strategy_or_decisions)` → in-sample metrics (the agent's feedback loop)
|
| 91 |
+
- `read_metrics()` → current equity/DD/Sharpe
|
| 92 |
+
- `@vf.stop`: agent submits final strategy, or max_turns.
|
| 93 |
+
- **Reward (Rubric, weighted) — computed on OOS, aggregated over n_windows × basket:**
|
| 94 |
+
|
| 95 |
+
| reward fn | weight | source |
|
| 96 |
+
|---|---|---|
|
| 97 |
+
| `r_sharpe` (normalized OOS Sharpe) | 0.40 | spine |
|
| 98 |
+
| `r_beats_benchmark` (vs buy-and-hold) | 0.20 | anti-overfit |
|
| 99 |
+
| `r_drawdown` (penalty for deep DD) | 0.15 | MEMORY: protect capital |
|
| 100 |
+
| `r_rr_discipline` (R:R≥2 compliance) | 0.10 | trading_agent._validate |
|
| 101 |
+
| `r_exposure` (sane equity/cash, no all-in) | 0.10 | MEMORY: sizing |
|
| 102 |
+
| `r_cost` (turnover/fee penalty) | 0.05 | realism |
|
| 103 |
+
| **HARD GATES → reward 0** | — | invalid code, lookahead, NaN equity, illiquid trades |
|
| 104 |
+
|
| 105 |
+
Outcome terms (sharpe + beats_benchmark) must dominate so the model can't fake
|
| 106 |
+
discipline while losing money.
|
| 107 |
+
|
| 108 |
+
## 6. Training (Prime Hosted, FREE for Laguna)
|
| 109 |
+
```toml
|
| 110 |
+
model = "poolside/Laguna-XS.2" # validate first on Qwen/Qwen3-4B-Instruct-2507
|
| 111 |
+
max_steps = 50 # small validation; scale after curve moves
|
| 112 |
+
batch_size = 128 # = parallel rollouts (the "128 sessions")
|
| 113 |
+
rollouts_per_example = 8 # GRPO group — needs decorrelated windows
|
| 114 |
+
learning_rate = 1e-4
|
| 115 |
+
lora_alpha = 16
|
| 116 |
+
[sampling]
|
| 117 |
+
max_tokens = 512
|
| 118 |
+
enable_thinking = false # docs: start non-reasoning for agentic tasks
|
| 119 |
+
[[env]]
|
| 120 |
+
id = "<user>/stock-strategy-env"
|
| 121 |
+
[eval]
|
| 122 |
+
eval_base_model = true # base-vs-trained comparison built in
|
| 123 |
+
```
|
| 124 |
+
Pipeline: `prime eval run` (sanity, baseline 10–80%) → small Qwen RL → Laguna RL →
|
| 125 |
+
download LoRA → `prime deployments create` → `base:adapter_id`.
|
| 126 |
+
|
| 127 |
+
## 7. Closing the loop
|
| 128 |
+
- Deploy adapter → point tradewatch `HybrieClient` at `api.pinference.ai` → **live paper demo**.
|
| 129 |
+
- **Ablation (money shot):** trained adapter, MEMORY.md stripped from prompt → discipline holds.
|
| 130 |
+
- **Recursive (stretch):** `checkpoint_id` warm-start → reflect on failures →
|
| 131 |
+
adjust rubric weights / add tasks / `[buffer]` difficulty filtering → retrain.
|
| 132 |
+
|
| 133 |
+
## 8. Non-negotiable risks
|
| 134 |
+
1. Leakage → structural causal feed (not a detector).
|
| 135 |
+
2. Reward variance → aggregate over basket × windows (or GRPO has no signal).
|
| 136 |
+
3. Reward hacking → OOS + must-beat-benchmark + hard lookahead gate.
|
| 137 |
+
4. Crypto noise → slippage/fee/liquidity model on every fill.
|
baseline_eval.txt
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Resolved source: stimulir/trade-pool
|
| 2 |
+
Eval job_id: trade_pool_poolside_laguna_xs.2_20260530_112447_9949ef5b
|
| 3 |
+
eval_dataset is not set, falling back to train dataset
|
| 4 |
+
|
| 5 |
+
--- Evaluation ---
|
| 6 |
+
Environment: trade-pool
|
| 7 |
+
Model: poolside/laguna-xs.2
|
| 8 |
+
Provider: https://api.pinference.ai/api/v1
|
| 9 |
+
Examples: 4
|
| 10 |
+
Rollouts per example: 1
|
| 11 |
+
--- Example ---
|
| 12 |
+
╭─────────────────────────────────── Step 0 ───────────────────────────────────╮
|
| 13 |
+
│ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓ │
|
| 14 |
+
│ ┃ Prompt ┃ Completion ┃ Reward ┃ │
|
| 15 |
+
│ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩ │
|
| 16 |
+
│ │ system: You are a quantitative │ assistant: │ 0.91 │ │
|
| 17 |
+
│ │ trading strategy engineer. You │ [reasoning] │ │ │
|
| 18 |
+
│ │ write a Python function that │ │ │ │
|
| 19 |
+
│ │ decides a target position each │ Let me analyze the price data │ │ │
|
| 20 |
+
│ │ bar, using ONLY causal │ and develop a strategy that │ │ │
|
| 21 |
+
│ │ technical features. │ follows the given rules. │ │ │
|
| 22 |
+
│ │ │ │ │ │
|
| 23 |
+
│ │ You must output a single │ Looking at the recent 30 │ │ │
|
| 24 |
+
│ │ Python code block defining │ closes, I can see the price │ │ │
|
| 25 |
+
│ │ exactly: │ has been declining from around │ │ │
|
| 26 |
+
│ │ │ 0.0245 to 0.0177, which is a │ │ │
|
| 27 |
+
│ │ ```python │ significant drop. This could │ │ │
|
| 28 |
+
│ │ def strategy(features, │ be a "post-dip curl" setup if │ │ │
|
| 29 |
+
│ │ position): │ we see a recovery. │ │ │
|
| 30 |
+
│ │ # features: dict of the │ │ │ │
|
| 31 |
+
│ │ current bar's indicators (see │ Let me design a strategy that: │ │ │
|
| 32 |
+
│ │ list below), all causal │ 1. Uses RSI to identify │ │ │
|
| 33 |
+
│ │ # position: your current │ oversold conditions │ │ │
|
| 34 |
+
│ │ target position, a float in │ 2. Uses MACD for momentum │ │ │
|
| 35 |
+
│ │ [-1, 1] │ confirmation │ │ │
|
| 36 |
+
│ │ # return: new target │ 3. Uses moving averages for │ │ │
|
| 37 |
+
│ │ position, float in [-1, 1] │ trend direction │ │ │
|
| 38 |
+
│ │ (1=full long, -1=full short, │ 4. Uses Bollinger Bands for │ │ │
|
| 39 |
+
│ │ 0=flat) │ position sizing │ │ │
|
| 40 |
+
│ │ ... │ 5. Incorporates volatility for │ │ │
|
| 41 |
+
│ │ return new_position │ risk management │ │ │
|
| 42 |
+
│ │ ``` │ 6. Follows the capital │ │ │
|
| 43 |
+
│ │ │ protection rules │ │ │
|
| 44 |
+
│ │ Available features (floats, │ │ │ │
|
| 45 |
+
│ │ computed on data up to the │ Key patterns to look for: │ │ │
|
| 46 |
+
│ │ current bar only): │ - Post-dip curl: RSI rising │ │ │
|
| 47 |
+
│ │ close, sma_10, sma_20, sma_50, │ from oversold (<30), MACD hist │ │ │
|
| 48 |
+
│ │ ema_12, ema_26, rsi_14, macd, │ turning positive after a drop │ │ │
|
| 49 |
+
│ │ macd_signal, macd_hist, │ - Healthy momentum: SMA_10 > │ │ │
|
| 50 |
+
│ │ zscore_20, bb_lo, bb_mid, │ SMA_20, MACD > MACD_signal │ │ │
|
| 51 |
+
│ │ bb_hi, ret_1, vol_20 │ - Avoid: Price too far from │ │ │
|
| 52 |
+
│ │ │ Bollinger Mid, high │ │ │
|
| 53 |
+
│ │ Hard rules: │ volatility, thin liquidity │ │ │
|
| 54 |
+
│ │ - No imports, no IO, no future │ │ │ │
|
| 55 |
+
│ │ data. You only get the current │ │ │ │
|
| 56 |
+
│ │ bar's features. │ │ │ │
|
| 57 |
+
│ │ - Favor capital protection: │ ```python │ │ │
|
| 58 |
+
│ │ control drawdown, avoid being │ def strategy(features, │ │ │
|
| 59 |
+
│ │ all-in blindly. │ position): │ │ │
|
| 60 |
+
│ │ - Your strategy is scored on a │ # Extract features │ │ │
|
| 61 |
+
│ │ HELD-OUT window and must beat │ close = features['close'] │ │ │
|
| 62 |
+
│ │ buy-and-hold. │ sma_10 = │ │ │
|
| 63 |
+
│ │ │ features['sma_10'] │ │ │
|
| 64 |
+
│ │ Trading discipline distilled │ sma_20 = │ │ │
|
| 65 |
+
│ │ from 618 real prior trading │ features['sma_20'] │ │ │
|
| 66 |
+
│ │ decisions (94 BUY, 489 WATCH, │ sma_50 = │ │ │
|
| 67 |
+
│ │ 35 AVOID): │ features['sma_50'] │ │ │
|
| 68 |
+
│ │ │ ema_12 = │ │ │
|
| 69 |
+
│ │ Patterns that earned a BUY │ features['ema_12'] │ │ │
|
| 70 |
+
│ │ (translate these into your │ ema_26 = │ │ │
|
| 71 |
+
│ │ strategy's entry logic): │ features['ema_26'] │ │ │
|
| 72 |
+
│ │ - "Post-dip curl": a 24h drop │ rsi_14 = │ │ │
|
| 73 |
+
│ │ followed by a 1h recovery with │ features['rsi_14'] │ │ │
|
| 74 |
+
│ │ a strong short-term buy/sell │ macd = features['macd'] │ │ │
|
| 75 |
+
│ │ ratio (>1.5). Enter on the │ macd_signal = │ │ │
|
| 76 |
+
│ │ curl, not the dip. → buy when │ features['macd_signal'] │ │ │
|
| 77 |
+
│ │ recent return turns up after a │ macd_hist = │ │ │
|
| 78 |
+
│ │ drawdown and momentum (rsi │ features['macd_hist'] │ │ │
|
| 79 |
+
│ │ rising from oversold, │ zscore_20 = │ │ │
|
| 80 |
+
│ │ macd_hist turning positive) │ features['zscore_20'] │ │ │
|
| 81 |
+
│ │ confirms. │ bb_lo = features['bb_lo'] │ │ │
|
| 82 |
+
│ │ - "Healthy momentum │ bb_mid = │ │ │
|
| 83 |
+
│ │ continuation": positive 1h AND │ features['bb_mid'] │ │ │
|
| 84 |
+
│ │ 24h change with buy pressure │ bb_hi = features['bb_hi'] │ │ │
|
| 85 |
+
│ │ and │ ret_1 = features['ret_1'] │ │ │
|
| 86 |
+
│ │ volume/liquidity > 1. → │ vol_20 = │ │ │
|
| 87 |
+
│ │ trend-follow when sma_10 > │ features['vol_20'] │ │ │
|
| 88 |
+
│ │ sma_20 and macd > macd_signal. │ │ │ │
|
| 89 |
+
│ │ - Size up only with liquidity │ # Initialize new position │ │ │
|
| 90 |
+
│ │ + volume backing the move; │ new_position = position │ │ │
|
| 91 |
+
│ │ size down when volatility is │ │ │ │
|
| 92 |
+
│ │ high. │ # Capital protection: │ │ │
|
| 93 |
+
│ │ │ avoid overexposure when │ │ │
|
| 94 |
+
│ │ Conditions that earned │ already up significantly │ │ │
|
| 95 |
+
│ │ WATCH/AVOID (translate into │ if ret_1 > 0.05: # │ │ │
|
| 96 |
+
│ │ your exits and risk limits): │ Already up 5% on the hour │ │ │
|
| 97 |
+
│ │ - Already +30% on the day → │ return min(position, │ │ │
|
| 98 |
+
│ │ entry timing is poor; do not │ 0.3) # Reduce exposure │ │ │
|
| 99 |
+
│ │ chase. → avoid full long when │ │ │ │
|
| 100 |
+
│ │ ret_1 is extremely high or │ # Avoid full long when │ │ │
|
| 101 |
+
│ │ price is far above bb_mid. │ price is far above middle │ │ │
|
| 102 |
+
│ │ - Volume/liquidity < 1.0 or │ Bollinger Band │ │ │
|
| 103 |
+
│ │ thin liquidity → lower │ if close > bb_mid * 1.03: │ │ │
|
| 104 |
+
│ │ conviction; reduce exposure. │ # 3% above BB mid │ │ │
|
| 105 |
+
│ │ - Sell-heavy short-term flow │ return max(position, │ │ │
|
| 106 |
+
│ │ after entry → cut quickly; │ -0.2) # Cap long exposure │ │ │
|
| 107 |
+
│ │ don't wait for a vague │ │ │ │
|
| 108 |
+
│ │ recovery. │ # Base position sizing │ │ │
|
| 109 |
+
│ │ │ inversely related to │ │ │
|
| 110 |
+
│ │ Capital-protection rules │ volatility │ │ │
|
| 111 |
+
│ │ (every BUY required these): │ vol_factor = max(0.5, 1.0 │ │ │
|
| 112 |
+
│ │ - Always define a stop below │ - vol_20) # Reduce size when │ │ │
|
| 113 |
+
│ │ entry and a target above; │ volatility high │ │ │
|
| 114 |
+
│ │ favor reward:risk >= 2:1. │ │ │ │
|
| 115 |
+
│ │ - Protect capital first: │ # Post-dip curl pattern: │ │ │
|
| 116 |
+
│ │ control drawdown over chasing │ RSI coming back from oversold │ │ │
|
| 117 |
+
│ │ upside. │ with MACD improvement │ │ │
|
| 118 |
+
│ │ - Don't average down losers; │ dip_recovery = False │ │ │
|
| 119 |
+
│ │ don't let a winner round-trip │ if rsi_14 < 30 and │ │ │
|
| 120 |
+
│ │ — tighten when flow weakens. │ macd_hist > 0: # Oversold │ │ │
|
| 121 |
+
│ │ │ with positive momentum │ │ │
|
| 122 |
+
│ │ │ dip_recovery = True │ │ │
|
| 123 |
+
│ │ │ │ │ │
|
| 124 |
+
│ │ user: Token: NOCK │ # Healthy momentum: short │ │ │
|
| 125 |
+
│ │ In-sample bars available: 600 │ and medium term trend │ │ │
|
| 126 |
+
│ │ Recent closes (last 30): │ confirmation │ │ │
|
| 127 |
+
│ │ [0.024502, 0.02227, 0.022973, │ momentum_ok = sma_10 > │ │ │
|
| 128 |
+
│ │ 0.022138, 0.022407, 0.02138, │ sma_20 and macd > macd_signal │ │ │
|
| 129 |
+
│ │ 0.02204, 0.025153, 0.023984, │ │ │ │
|
| 130 |
+
│ │ 0.025721, 0.024264, 0.023896, │ # Trend following with │ │ │
|
| 131 |
+
│ │ 0.023152, 0.022478, 0.023024, │ moving averages │ │ │
|
| 132 |
+
│ │ 0.022491, 0.022522, 0.02344, │ trend_up = ema_12 > ema_26 │ │ │
|
| 133 |
+
│ │ 0.023711, 0.022644, 0.022412, │ and sma_20 > sma_50 │ │ │
|
| 134 |
+
│ │ 0.022769, 0.02276, 0.023428, │ │ │ │
|
| 135 |
+
│ │ 0.022117, 0.021182, 0.020521, │ # Entry signals │ │ │
|
| 136 |
+
│ │ 0.019539, 0.018417, 0.017665] │ if dip_recovery and │ │ │
|
| 137 |
+
│ │ │ momentum_ok: │ │ │
|
| 138 |
+
│ │ Write a `strategy(features, │ # Post-dip entry with │ │ │
|
| 139 |
+
│ │ position)` function. Use │ strong confirmation │ │ │
|
| 140 |
+
│ │ indicators like RSI (rsi_14), │ new_position = min(0.6 │ │ │
|
| 141 |
+
│ │ MACD │ * vol_factor, 0.6) │ │ │
|
| 142 |
+
│ │ (macd/macd_signal/macd_hist), │ elif trend_up and │ │ │
|
| 143 |
+
│ │ moving averages (sma_*/ema_*), │ momentum_ok and zscore_20 < │ │ │
|
| 144 |
+
│ │ z-score (zscore_20), Bollinger │ 1.5: │ │ │
|
| 145 |
+
│ │ bands (bb_*), and volatility │ # Trend continuation │ │ │
|
| 146 |
+
│ │ (vol_20). Return a target │ with reasonable valuation │ │ │
|
| 147 |
+
│ │ position in [-1, 1]. │ new_position = min(0.4 │ │ │
|
| 148 |
+
│ │ │ * vol_factor, 0.4) │ │ │
|
| 149 |
+
│ │ │ elif zscore_20 < -1.0 and │ │ │
|
| 150 |
+
│ │ │ macd_hist > -0.001: │ │ │
|
| 151 |
+
│ │ │ # Mean reversion setup │ │ │
|
| 152 |
+
│ │ │ near lower Bollinger Band │ │ │
|
| 153 |
+
│ │ │ new_position = min(0.3 │ │ │
|
| 154 |
+
│ │ │ * vol_factor, 0.3) │ │ │
|
| 155 |
+
│ │ │ else: │ │ │
|
| 156 |
+
│ │ │ # Reduce exposure if │ │ │
|
| 157 |
+
│ │ │ no clear signal │ │ │
|
| 158 |
+
│ │ │ new_position = │ │ │
|
| 159 |
+
│ │ │ position * 0.7 │ │ │
|
| 160 |
+
│ │ │ │ │ │
|
| 161 |
+
│ │ │ # Exit management: reduce │ │ │
|
| 162 |
+
│ │ │ position if momentum weakens │ │ │
|
| 163 |
+
│ │ │ if macd_hist < 0 and │ │ │
|
| 164 |
+
│ │ │ position > 0.2: │ │ │
|
| 165 |
+
│ │ │ new_position = │ │ │
|
| 166 |
+
│ │ │ max(new_position, -0.1) # │ │ │
|
| 167 |
+
│ │ │ Quick exit │ │ │
|
| 168 |
+
│ │ │ elif rsi_14 > 70 and │ │ │
|
| 169 |
+
│ │ │ position > 0.3: │ │ │
|
| 170 |
+
│ │ │ new_position = │ │ │
|
| 171 |
+
│ │ │ max(new_position, 0.0) # Take │ │ │
|
| 172 |
+
│ │ │ profits on overbought │ │ │
|
| 173 |
+
│ │ │ │ │ │
|
| 174 |
+
│ │ │ # Ensure position bounds │ │ │
|
| 175 |
+
│ │ │ new_position = max(-1.0, │ │ │
|
| 176 |
+
│ │ │ min(1.0, new_position)) │ │ │
|
| 177 |
+
│ │ │ │ │ │
|
| 178 |
+
│ │ │ return new_position │ │ │
|
| 179 |
+
│ │ │ ``` │ │ │
|
| 180 |
+
│ │ │ </assistant> │ │ │
|
| 181 |
+
│ └────────────────────────────────┴────────────────────────────────┴────────┘ │
|
| 182 |
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
| 183 |
+
--- All ---
|
| 184 |
+
Rewards:
|
| 185 |
+
reward: avg - 0.495, std - 0.328
|
| 186 |
+
r1: [0.915, 0.587, 0.0, 0.478]
|
| 187 |
+
reward_sharpe: avg - 0.448, std - 0.320
|
| 188 |
+
r1: [0.873, 0.577, 0.0, 0.344]
|
| 189 |
+
reward_beats_benchmark: avg - 0.250, std - 0.433
|
| 190 |
+
r1: [1.0, 0.0, 0.0, 0.0]
|
| 191 |
+
reward_drawdown: avg - 0.652, std - 0.381
|
| 192 |
+
r1: [0.943, 0.878, 0.0, 0.788]
|
| 193 |
+
reward_exposure: avg - 0.750, std - 0.433
|
| 194 |
+
r1: [1.0, 1.0, 0.0, 1.0]
|
| 195 |
+
reward_cost: avg - 0.355, std - 0.206
|
| 196 |
+
r1: [0.482, 0.5, 0.0, 0.438]
|
| 197 |
+
reward_valid: avg - 0.750, std - 0.433
|
| 198 |
+
r1: [1.0, 1.0, 0.0, 1.0]
|
| 199 |
+
num_turns: avg - 1.000, std - 0.000
|
| 200 |
+
r1: [1.0, 1.0, 1.0, 1.0]
|
| 201 |
+
Info:
|
| 202 |
+
is_truncated: avg - 0.000, std - 0.000
|
| 203 |
+
stop_conditions: max_turns_reached: 1.000
|
| 204 |
+
Timing:
|
| 205 |
+
total min - 6s, mean - 15s, max - 29s
|
| 206 |
+
setup min - 0ms, mean - 0ms, max - 0ms
|
| 207 |
+
generation min - 6s, mean - 15s, max - 29s
|
| 208 |
+
model min - 6s, mean - 15s, max - 29s
|
| 209 |
+
env min - 0ms, mean - 0ms, max - 0ms
|
| 210 |
+
scoring min - 1ms, mean - 41ms, max - 55ms
|
| 211 |
+
overhead min - 0ms, mean - 0ms, max - 1ms
|
| 212 |
+
Usage:
|
| 213 |
+
input_tokens (avg): 1061.500
|
| 214 |
+
output_tokens (avg): 2066.000
|
| 215 |
+
final_input_tokens (avg): 1061.500
|
| 216 |
+
final_output_tokens (avg): 2066.000
|
| 217 |
+
|
| 218 |
+
Performance:
|
| 219 |
+
event_loop_lag: mean=7ms, p99=3ms, max=2s (n=335)
|
configs/iter_1.toml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AUTO-GENERATED iteration 1 — recursive self-improving loop
|
| 2 |
+
# objective=sharpe focus_symbols=all warm_start=cold
|
| 3 |
+
model = "poolside/Laguna-XS.2"
|
| 4 |
+
max_steps = 50
|
| 5 |
+
batch_size = 128
|
| 6 |
+
rollouts_per_example = 8
|
| 7 |
+
learning_rate = 0.0001
|
| 8 |
+
lora_alpha = 16
|
| 9 |
+
# checkpoint_id = (none — cold start)
|
| 10 |
+
[sampling]
|
| 11 |
+
max_tokens = 768
|
| 12 |
+
enable_thinking = false
|
| 13 |
+
|
| 14 |
+
[[env]]
|
| 15 |
+
id = "stimulir/trade-pool"
|
| 16 |
+
args = { split = "train", seed = 0, objective = "sharpe" }
|
| 17 |
+
|
| 18 |
+
[eval]
|
| 19 |
+
interval = 25
|
| 20 |
+
eval_base_model = true
|
| 21 |
+
[[eval.env]]
|
| 22 |
+
id = "stimulir/trade-pool"
|
| 23 |
+
args = { split = "oos_symbols", seed = 0, objective = "sharpe" }
|
| 24 |
+
|
| 25 |
+
[adapters]
|
| 26 |
+
interval = 0
|
configs/laguna-stock-strategy.toml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RL training config — coding agent writes causal crypto strategies, scored by OOS backtest.
|
| 2 |
+
# Validate on Qwen first (cheap), then swap model to poolside/Laguna-XS.2 (FREE hosted).
|
| 3 |
+
|
| 4 |
+
model = "Qwen/Qwen3-4B-Instruct-2507" # → "poolside/Laguna-XS.2" once env is validated
|
| 5 |
+
max_steps = 50 # small validation run; scale after reward curve moves
|
| 6 |
+
batch_size = 128 # parallel rollouts/step (the "128 sessions") — GRPO group source
|
| 7 |
+
rollouts_per_example = 8 # GRPO group size; decorrelated tasks supply variance
|
| 8 |
+
learning_rate = 1e-4
|
| 9 |
+
lora_alpha = 16
|
| 10 |
+
|
| 11 |
+
[sampling]
|
| 12 |
+
max_tokens = 768 # room to write a strategy function
|
| 13 |
+
enable_thinking = false # docs: start non-reasoning for agentic tasks
|
| 14 |
+
|
| 15 |
+
[[env]]
|
| 16 |
+
id = "REPLACE_WITH/stock-strategy-env" # set after `prime env push`
|
| 17 |
+
args = { split = "train", seed = 0 }
|
| 18 |
+
|
| 19 |
+
[eval]
|
| 20 |
+
interval = 25
|
| 21 |
+
eval_base_model = true # base-vs-trained comparison built in
|
| 22 |
+
[[eval.env]]
|
| 23 |
+
id = "REPLACE_WITH/stock-strategy-env"
|
| 24 |
+
args = { split = "oos_symbols", seed = 0 } # generalization: HELD-OUT symbols
|
| 25 |
+
|
| 26 |
+
[adapters]
|
| 27 |
+
interval = 0 # upload LoRA adapter at run end
|
reward_curve.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
step 49: 0.3657
|
trade_pool/__init__.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""trade_pool — a verifiers RL environment where a coding agent writes a
|
| 2 |
+
causal crypto trading strategy, scored by an out-of-sample backtest.
|
| 3 |
+
|
| 4 |
+
The agent receives a task (a token's in-sample price history + the feature surface),
|
| 5 |
+
writes a Python `strategy(features, position) -> target_position` function, and is
|
| 6 |
+
rewarded by how that strategy performs on a HELD-OUT window — must beat buy-and-hold,
|
| 7 |
+
control drawdown, and keep sane exposure. tradewatch's MEMORY.md discipline becomes
|
| 8 |
+
the rubric; RL turns it into adapter weights.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import verifiers as vf
|
| 17 |
+
from datasets import Dataset
|
| 18 |
+
|
| 19 |
+
from .backtester import benchmarks, run_backtest
|
| 20 |
+
from .data import build_tasks
|
| 21 |
+
from .executor import compile_strategy, extract_code
|
| 22 |
+
from .seed_principles import principles_block
|
| 23 |
+
|
| 24 |
+
FEATURE_NAMES = [
|
| 25 |
+
"close", "sma_10", "sma_20", "sma_50", "ema_12", "ema_26", "rsi_14",
|
| 26 |
+
"macd", "macd_signal", "macd_hist", "zscore_20", "bb_lo", "bb_mid", "bb_hi",
|
| 27 |
+
"ret_1", "vol_20",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
SYSTEM_PROMPT = """You are a quantitative trading strategy engineer. You write a Python \
|
| 31 |
+
function that decides a target position each bar, using ONLY causal technical features.
|
| 32 |
+
|
| 33 |
+
You must output a single Python code block defining exactly:
|
| 34 |
+
|
| 35 |
+
```python
|
| 36 |
+
def strategy(features, position):
|
| 37 |
+
# features: dict of the current bar's indicators (see list below), all causal
|
| 38 |
+
# position: your current target position, a float in [-1, 1]
|
| 39 |
+
# return: new target position, float in [-1, 1] (1=full long, -1=full short, 0=flat)
|
| 40 |
+
...
|
| 41 |
+
return new_position
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
Available features (floats, computed on data up to the current bar only):
|
| 45 |
+
{features}
|
| 46 |
+
|
| 47 |
+
Hard rules:
|
| 48 |
+
- No imports, no IO, no future data. You only get the current bar's features.
|
| 49 |
+
- Favor capital protection: control drawdown, avoid being all-in blindly.
|
| 50 |
+
- Your strategy is scored on a HELD-OUT window and must beat buy-and-hold.
|
| 51 |
+
|
| 52 |
+
{seed_principles}
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _task_to_prompt(task: dict) -> str:
|
| 57 |
+
close = task["train_close"]
|
| 58 |
+
tail = close[-30:] if len(close) > 30 else close
|
| 59 |
+
return (
|
| 60 |
+
f"Token: {task['symbol']}\n"
|
| 61 |
+
f"In-sample bars available: {len(close)}\n"
|
| 62 |
+
f"Recent closes (last {len(tail)}): {[round(c, 6) for c in tail]}\n\n"
|
| 63 |
+
"Write a `strategy(features, position)` function. Use indicators like RSI "
|
| 64 |
+
"(rsi_14), MACD (macd/macd_signal/macd_hist), moving averages (sma_*/ema_*), "
|
| 65 |
+
"z-score (zscore_20), Bollinger bands (bb_*), and volatility (vol_20). "
|
| 66 |
+
"Return a target position in [-1, 1]."
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# ── Rubric reward functions (each receives completion + the task info via state) ──
|
| 71 |
+
|
| 72 |
+
# Memoize per (symbol, code) so the 6 reward fns trigger ONE backtest set per rollout,
|
| 73 |
+
# not 6×(1 strategy + 3 benchmarks). Bounded so it can't grow unboundedly across a run.
|
| 74 |
+
_SCORE_CACHE: dict[tuple, dict] = {}
|
| 75 |
+
_PARTICIPATION_FLOOR = 0.03 # avg |exposure| below this = "did nothing", risk rewards void
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _score_strategy(completion, info) -> dict:
|
| 79 |
+
"""Compile + backtest the agent's strategy on the OOS window. Memoized."""
|
| 80 |
+
text = completion if isinstance(completion, str) else completion[-1]["content"]
|
| 81 |
+
code = extract_code(text)
|
| 82 |
+
key = (info["symbol"], hash(code))
|
| 83 |
+
if key in _SCORE_CACHE:
|
| 84 |
+
return _SCORE_CACHE[key]
|
| 85 |
+
|
| 86 |
+
fn, err = compile_strategy(code)
|
| 87 |
+
oos = np.asarray(info["oos_close"], dtype=float)
|
| 88 |
+
warmup = min(50, len(oos) // 3)
|
| 89 |
+
if fn is None:
|
| 90 |
+
out = {"error": err, "res": None, "bench": None}
|
| 91 |
+
else:
|
| 92 |
+
res = run_backtest(oos, fn, warmup=warmup)
|
| 93 |
+
bench = benchmarks(oos, warmup=warmup)
|
| 94 |
+
out = {"error": res.error, "res": res, "bench": bench}
|
| 95 |
+
if len(_SCORE_CACHE) > 4096:
|
| 96 |
+
_SCORE_CACHE.clear()
|
| 97 |
+
_SCORE_CACHE[key] = out
|
| 98 |
+
return out
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _participates(res) -> bool:
|
| 102 |
+
return res is not None and res.avg_exposure >= _PARTICIPATION_FLOOR
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def reward_sharpe(completion, info, **kwargs) -> float:
|
| 106 |
+
s = _score_strategy(completion, info)
|
| 107 |
+
if s["res"] is None or s["error"]:
|
| 108 |
+
return 0.0
|
| 109 |
+
# squash Sharpe to [0,1] via logistic; Sharpe 0 -> 0.5, 2 -> ~0.88.
|
| 110 |
+
# A flat strategy gets sharpe 0 -> 0.5; gate it so do-nothing can't bank 0.5.
|
| 111 |
+
if not _participates(s["res"]):
|
| 112 |
+
return 0.0
|
| 113 |
+
return float(1.0 / (1.0 + np.exp(-s["res"].sharpe)))
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def reward_beats_benchmark(completion, info, **kwargs) -> float:
|
| 117 |
+
s = _score_strategy(completion, info)
|
| 118 |
+
if s["res"] is None or s["error"] or s["bench"] is None or not _participates(s["res"]):
|
| 119 |
+
return 0.0
|
| 120 |
+
bh = s["bench"]["buy_and_hold"].sharpe
|
| 121 |
+
return 1.0 if s["res"].sharpe > bh else 0.0
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def reward_drawdown(completion, info, **kwargs) -> float:
|
| 125 |
+
# Drawdown reward only counts if the strategy actually traded — otherwise
|
| 126 |
+
# "do nothing" banks a perfect 1.0 for taking zero risk (the inactivity exploit).
|
| 127 |
+
s = _score_strategy(completion, info)
|
| 128 |
+
if s["res"] is None or s["error"] or not _participates(s["res"]):
|
| 129 |
+
return 0.0
|
| 130 |
+
return float(max(0.0, 1.0 - s["res"].max_drawdown))
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def reward_exposure(completion, info, **kwargs) -> float:
|
| 134 |
+
s = _score_strategy(completion, info)
|
| 135 |
+
if s["res"] is None or s["error"] or not _participates(s["res"]):
|
| 136 |
+
return 0.0
|
| 137 |
+
e = s["res"].avg_exposure
|
| 138 |
+
return 1.0 if 0.1 <= e <= 0.85 else max(0.0, 1.0 - abs(e - 0.5))
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def reward_cost(completion, info, **kwargs) -> float:
|
| 142 |
+
# Likewise gated: zero turnover (no trades) must not earn the low-cost reward.
|
| 143 |
+
s = _score_strategy(completion, info)
|
| 144 |
+
if s["res"] is None or s["error"] or not _participates(s["res"]):
|
| 145 |
+
return 0.0
|
| 146 |
+
return float(max(0.0, 1.0 - s["res"].turnover / 20.0))
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def reward_valid(completion, info, **kwargs) -> float:
|
| 150 |
+
# Valid = compiles AND actually trades. A syntactically-valid do-nothing is not
|
| 151 |
+
# a valid trading strategy for our purposes.
|
| 152 |
+
s = _score_strategy(completion, info)
|
| 153 |
+
if s["res"] is None or s["error"]:
|
| 154 |
+
return 0.0
|
| 155 |
+
return 1.0 if _participates(s["res"]) else 0.0
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# Objective presets = rubric weight vectors. The recursive self-improving loop selects
|
| 159 |
+
# an objective (or passes explicit reward_weights) per iteration, so curriculum can shift
|
| 160 |
+
# emphasis (e.g. toward drawdown control if the trained model is taking too much risk)
|
| 161 |
+
# WITHOUT rebuilding the wheel — these arrive via the TOML [[env]] args block.
|
| 162 |
+
# [sharpe, beats_bh, drawdown, exposure, cost, valid]
|
| 163 |
+
OBJECTIVE_WEIGHTS = {
|
| 164 |
+
"sharpe": [0.40, 0.20, 0.15, 0.10, 0.05, 0.10],
|
| 165 |
+
"min_drawdown": [0.20, 0.15, 0.35, 0.15, 0.05, 0.10],
|
| 166 |
+
"balanced": [0.30, 0.25, 0.20, 0.10, 0.05, 0.10],
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def load_environment(
|
| 171 |
+
split: str = "train",
|
| 172 |
+
seed: int = 0,
|
| 173 |
+
objective: str = "sharpe",
|
| 174 |
+
reward_weights: list[float] | None = None,
|
| 175 |
+
symbols: list[str] | None = None,
|
| 176 |
+
use_seed_principles: bool = True,
|
| 177 |
+
max_turns: int = 1,
|
| 178 |
+
) -> vf.Environment:
|
| 179 |
+
"""Entry point Prime calls (args come from the TOML [[env]] args block).
|
| 180 |
+
|
| 181 |
+
Curriculum knobs for the recursive loop:
|
| 182 |
+
split train | oos | oos_symbols (which symbol pool)
|
| 183 |
+
seed rotates the symbol shuffle (exposes new task mixes per iteration)
|
| 184 |
+
objective sharpe | min_drawdown | balanced (rubric weight preset)
|
| 185 |
+
reward_weights explicit 6-vector, overrides objective
|
| 186 |
+
symbols restrict to these symbols (curriculum: focus on weak performers)
|
| 187 |
+
use_seed_principles inject tradewatch's 618-decision discipline block (default on);
|
| 188 |
+
set False for the "memory is the adapter" ablation (strip the prompt
|
| 189 |
+
discipline and test whether trained weights retain it)
|
| 190 |
+
"""
|
| 191 |
+
tasks = build_tasks(split=split, seed=seed)
|
| 192 |
+
if symbols:
|
| 193 |
+
wanted = {s.upper() for s in symbols}
|
| 194 |
+
tasks = [t for t in tasks if t["symbol"].upper() in wanted] or tasks
|
| 195 |
+
seed_block = principles_block() if use_seed_principles else ""
|
| 196 |
+
system = SYSTEM_PROMPT.format(features=", ".join(FEATURE_NAMES), seed_principles=seed_block)
|
| 197 |
+
rows = []
|
| 198 |
+
for t in tasks:
|
| 199 |
+
rows.append({
|
| 200 |
+
"prompt": [
|
| 201 |
+
{"role": "system", "content": system},
|
| 202 |
+
{"role": "user", "content": _task_to_prompt(t)},
|
| 203 |
+
],
|
| 204 |
+
"info": {"symbol": t["symbol"], "oos_close": t["oos_close"]},
|
| 205 |
+
})
|
| 206 |
+
dataset = Dataset.from_list(rows)
|
| 207 |
+
|
| 208 |
+
weights = reward_weights or OBJECTIVE_WEIGHTS.get(objective, OBJECTIVE_WEIGHTS["sharpe"])
|
| 209 |
+
rubric = vf.Rubric(
|
| 210 |
+
funcs=[reward_sharpe, reward_beats_benchmark, reward_drawdown,
|
| 211 |
+
reward_exposure, reward_cost, reward_valid],
|
| 212 |
+
weights=weights,
|
| 213 |
+
)
|
| 214 |
+
return vf.SingleTurnEnv(dataset=dataset, rubric=rubric)
|
trade_pool/backtester.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Causal replay backtester — the RL verifier's beating heart.
|
| 2 |
+
|
| 3 |
+
Ports tradewatch's paper_ledger.py accounting into a vectorised, deterministic,
|
| 4 |
+
LEAK-PROOF backtest. A strategy is a pure function:
|
| 5 |
+
|
| 6 |
+
def strategy(features_up_to_t: dict[str, float], position: float) -> float
|
| 7 |
+
# returns target position in [-1, 1] (fraction of equity; <0 = short)
|
| 8 |
+
|
| 9 |
+
The engine calls it bar-by-bar, passing ONLY features at indices <= t. The strategy
|
| 10 |
+
can never see the future because future bars are not in the dict it receives. Fills
|
| 11 |
+
happen at the NEXT bar's open-equivalent (close[t] used as decision price, close[t+1]
|
| 12 |
+
as fill realisation) with slippage + fees, so a decision at t cannot be filled at t's
|
| 13 |
+
hindsight-perfect price.
|
| 14 |
+
|
| 15 |
+
Metrics: Sharpe (primary), CAGR, max drawdown, equity/cash exposure, turnover, win rate.
|
| 16 |
+
Benchmarks: buy-and-hold, MA-crossover, z-score mean-reversion.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
from dataclasses import dataclass, field
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
|
| 25 |
+
from .features import feature_frame, sma, zscore
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class BacktestResult:
|
| 30 |
+
equity_curve: np.ndarray
|
| 31 |
+
returns: np.ndarray
|
| 32 |
+
sharpe: float
|
| 33 |
+
cagr: float
|
| 34 |
+
max_drawdown: float
|
| 35 |
+
turnover: float
|
| 36 |
+
win_rate: float
|
| 37 |
+
final_equity: float
|
| 38 |
+
avg_exposure: float
|
| 39 |
+
n_trades: int
|
| 40 |
+
error: str | None = None
|
| 41 |
+
meta: dict = field(default_factory=dict)
|
| 42 |
+
|
| 43 |
+
def to_dict(self) -> dict:
|
| 44 |
+
return {
|
| 45 |
+
"sharpe": round(self.sharpe, 4),
|
| 46 |
+
"cagr": round(self.cagr, 4),
|
| 47 |
+
"max_drawdown": round(self.max_drawdown, 4),
|
| 48 |
+
"turnover": round(self.turnover, 4),
|
| 49 |
+
"win_rate": round(self.win_rate, 4),
|
| 50 |
+
"final_equity": round(self.final_equity, 4),
|
| 51 |
+
"avg_exposure": round(self.avg_exposure, 4),
|
| 52 |
+
"n_trades": self.n_trades,
|
| 53 |
+
"error": self.error,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# Periods-per-year for annualisation (daily bars by default; 24*365 for hourly).
|
| 58 |
+
def _ann_factor(bars_per_year: float) -> float:
|
| 59 |
+
return float(np.sqrt(bars_per_year))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _metrics(equity: np.ndarray, exposures: np.ndarray, bars_per_year: float,
|
| 63 |
+
error: str | None = None) -> BacktestResult:
|
| 64 |
+
equity = np.asarray(equity, dtype=float)
|
| 65 |
+
if len(equity) < 2 or np.any(~np.isfinite(equity)) or np.any(equity <= 0):
|
| 66 |
+
return BacktestResult(equity, np.zeros(1), 0.0, 0.0, 1.0, 0.0, 0.0,
|
| 67 |
+
float(equity[-1]) if len(equity) else 0.0, 0.0, 0,
|
| 68 |
+
error=error or "degenerate equity")
|
| 69 |
+
rets = np.diff(equity) / equity[:-1]
|
| 70 |
+
sd = rets.std()
|
| 71 |
+
sharpe = float(rets.mean() / sd * _ann_factor(bars_per_year)) if sd > 1e-12 else 0.0
|
| 72 |
+
n = len(equity)
|
| 73 |
+
years = n / bars_per_year
|
| 74 |
+
cagr = float((equity[-1] / equity[0]) ** (1 / years) - 1) if years > 0 and equity[0] > 0 else 0.0
|
| 75 |
+
peak = np.maximum.accumulate(equity)
|
| 76 |
+
max_dd = float(np.max((peak - equity) / peak)) if len(peak) else 0.0
|
| 77 |
+
exp = np.asarray(exposures, dtype=float)
|
| 78 |
+
turnover = float(np.sum(np.abs(np.diff(np.insert(exp, 0, 0.0)))))
|
| 79 |
+
n_trades = int(np.sum(np.abs(np.diff(np.insert(exp, 0, 0.0))) > 1e-9))
|
| 80 |
+
win_rate = float(np.mean(rets > 0)) if len(rets) else 0.0
|
| 81 |
+
return BacktestResult(equity, rets, sharpe, cagr, max_dd, turnover, win_rate,
|
| 82 |
+
float(equity[-1]), float(np.mean(np.abs(exp))), n_trades, error=error)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def run_backtest(
|
| 86 |
+
close: np.ndarray,
|
| 87 |
+
strategy_fn,
|
| 88 |
+
*,
|
| 89 |
+
fee_bps: float = 30.0, # 0.30% per unit turnover (DEX-realistic)
|
| 90 |
+
slippage_bps: float = 20.0, # 0.20% adverse slippage on fills
|
| 91 |
+
bars_per_year: float = 365.0,
|
| 92 |
+
allow_short: bool = True,
|
| 93 |
+
warmup: int = 50, # bars reserved for feature warmup (no trading)
|
| 94 |
+
) -> BacktestResult:
|
| 95 |
+
"""Step the strategy causally over `close`, return performance metrics.
|
| 96 |
+
|
| 97 |
+
Fill model: decision made on info up to t, position realised against return
|
| 98 |
+
from t -> t+1, minus fees/slippage proportional to position change.
|
| 99 |
+
"""
|
| 100 |
+
close = np.asarray(close, dtype=float)
|
| 101 |
+
if len(close) < warmup + 5:
|
| 102 |
+
return _metrics(np.array([1.0, 1.0]), np.zeros(1), bars_per_year, error="too few bars")
|
| 103 |
+
|
| 104 |
+
feats = feature_frame(close)
|
| 105 |
+
rets = np.zeros(len(close))
|
| 106 |
+
rets[1:] = np.diff(close) / np.where(close[:-1] == 0, np.nan, close[:-1])
|
| 107 |
+
rets = np.nan_to_num(rets)
|
| 108 |
+
|
| 109 |
+
equity = [1.0]
|
| 110 |
+
exposures = []
|
| 111 |
+
pos = 0.0
|
| 112 |
+
cost_rate = (fee_bps + slippage_bps) / 1e4
|
| 113 |
+
|
| 114 |
+
for t in range(warmup, len(close) - 1):
|
| 115 |
+
# features visible at t (scalar snapshot, causal by construction)
|
| 116 |
+
view = {k: (float(v[t]) if np.isfinite(v[t]) else 0.0) for k, v in feats.items()}
|
| 117 |
+
try:
|
| 118 |
+
target = float(strategy_fn(view, pos))
|
| 119 |
+
except Exception as e: # noqa: BLE001 - strategy bugs => penalised, not crash
|
| 120 |
+
return _metrics(np.array(equity + [equity[-1]]), np.array(exposures + [pos]),
|
| 121 |
+
bars_per_year, error=f"strategy raised: {type(e).__name__}: {e}")
|
| 122 |
+
if not np.isfinite(target):
|
| 123 |
+
target = 0.0
|
| 124 |
+
lo = -1.0 if allow_short else 0.0
|
| 125 |
+
target = max(lo, min(1.0, target))
|
| 126 |
+
|
| 127 |
+
trade_cost = abs(target - pos) * cost_rate
|
| 128 |
+
# equity evolves with NEXT bar's realised return at the chosen exposure
|
| 129 |
+
pnl = target * rets[t + 1]
|
| 130 |
+
new_equity = equity[-1] * (1.0 + pnl - trade_cost)
|
| 131 |
+
equity.append(new_equity)
|
| 132 |
+
exposures.append(target)
|
| 133 |
+
pos = target
|
| 134 |
+
|
| 135 |
+
return _metrics(np.array(equity), np.array(exposures), bars_per_year)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# ── Benchmarks (the anti-overfit baselines a strategy must beat) ──────────────
|
| 139 |
+
|
| 140 |
+
def _buy_and_hold(view, pos):
|
| 141 |
+
return 1.0
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _ma_crossover(view, pos):
|
| 145 |
+
return 1.0 if view.get("sma_10", 0) > view.get("sma_20", 0) else 0.0
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _zscore_meanrev(view, pos):
|
| 149 |
+
z = view.get("zscore_20", 0.0)
|
| 150 |
+
if z < -1.5:
|
| 151 |
+
return 1.0
|
| 152 |
+
if z > 1.5:
|
| 153 |
+
return -1.0
|
| 154 |
+
return pos # hold
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def benchmarks(close: np.ndarray, **kw) -> dict[str, BacktestResult]:
|
| 158 |
+
return {
|
| 159 |
+
"buy_and_hold": run_backtest(close, _buy_and_hold, **kw),
|
| 160 |
+
"ma_crossover": run_backtest(close, _ma_crossover, **kw),
|
| 161 |
+
"zscore_meanrev": run_backtest(close, _zscore_meanrev, **kw),
|
| 162 |
+
}
|
trade_pool/data.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load the replay tape and build the train / OOS / OOS-symbol task splits.
|
| 2 |
+
|
| 3 |
+
Shallow per-symbol history (free GeckoTerminal cap ~1000 bars) means breadth carries
|
| 4 |
+
the GRPO variance: a task = (symbol, window). The symbol-holdout split is the real
|
| 5 |
+
generalization test.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import glob
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import pandas as pd
|
| 17 |
+
|
| 18 |
+
# Resolve the tape relative to THIS file so it works after the env is built into a
|
| 19 |
+
# wheel and installed into site-packages on Prime's infra (where the repo's top-level
|
| 20 |
+
# data/ dir does not exist). Packaged copy is authoritative; repo dir is a dev fallback.
|
| 21 |
+
_PKG_DATA = Path(__file__).resolve().parent / "data" / "ohlcv"
|
| 22 |
+
_REPO_DATA = Path(__file__).resolve().parents[3] / "data" / "ohlcv"
|
| 23 |
+
DATA = _PKG_DATA if _PKG_DATA.exists() and any(_PKG_DATA.glob("*.parquet")) else _REPO_DATA
|
| 24 |
+
|
| 25 |
+
# Quote-side / blue-chip pairs that aren't tradeable "strategy" targets — excluded.
|
| 26 |
+
_EXCLUDE = {"USDC", "WETH", "CBBTC", "USDT", "DAI"}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _load_series() -> dict[str, np.ndarray]:
|
| 30 |
+
series: dict[str, np.ndarray] = {}
|
| 31 |
+
for f in sorted(glob.glob(str(DATA / "*.parquet"))):
|
| 32 |
+
sym = os.path.basename(f).split("__")[0].upper()
|
| 33 |
+
if sym in _EXCLUDE:
|
| 34 |
+
continue
|
| 35 |
+
close = pd.read_parquet(f)["c"].to_numpy(dtype=float)
|
| 36 |
+
if len(close) >= 120 and np.all(np.isfinite(close)) and np.all(close > 0):
|
| 37 |
+
# keep the most liquid pool per symbol (first sorted = arbitrary but stable)
|
| 38 |
+
if sym not in series or len(close) > len(series[sym]):
|
| 39 |
+
series[sym] = close
|
| 40 |
+
return series
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def build_tasks(split: str = "train", oos_frac: float = 0.4, seed: int = 0) -> list[dict]:
|
| 44 |
+
"""Return a list of task dicts: {symbol, train_close, oos_close}.
|
| 45 |
+
|
| 46 |
+
- train: symbols sorted, first 70% of symbols; in-sample = early bars, oos = late bars
|
| 47 |
+
- oos: same symbols, evaluated on held-out late window
|
| 48 |
+
- oos_symbols: the held-out 30% of symbols (never seen in training)
|
| 49 |
+
"""
|
| 50 |
+
series = _load_series()
|
| 51 |
+
symbols = sorted(series.keys())
|
| 52 |
+
rng = np.random.default_rng(seed)
|
| 53 |
+
rng.shuffle(symbols)
|
| 54 |
+
cut = max(1, int(len(symbols) * 0.7))
|
| 55 |
+
train_syms, holdout_syms = symbols[:cut], symbols[cut:]
|
| 56 |
+
use = holdout_syms if split == "oos_symbols" else train_syms
|
| 57 |
+
|
| 58 |
+
tasks = []
|
| 59 |
+
for sym in use:
|
| 60 |
+
close = series[sym]
|
| 61 |
+
split_at = int(len(close) * (1 - oos_frac))
|
| 62 |
+
tasks.append({
|
| 63 |
+
"symbol": sym,
|
| 64 |
+
"train_close": close[:split_at].tolist(),
|
| 65 |
+
"oos_close": close[split_at:].tolist(),
|
| 66 |
+
"n_bars": len(close),
|
| 67 |
+
})
|
| 68 |
+
return tasks
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def universe_summary() -> dict:
|
| 72 |
+
series = _load_series()
|
| 73 |
+
return {"n_symbols": len(series), "symbols": sorted(series.keys()),
|
| 74 |
+
"bars": {k: len(v) for k, v in series.items()}}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
print(json.dumps(universe_summary(), indent=2))
|
| 79 |
+
for split in ("train", "oos_symbols"):
|
| 80 |
+
t = build_tasks(split)
|
| 81 |
+
print(f"{split}: {len(t)} tasks -> {[x['symbol'] for x in t]}")
|
trade_pool/executor.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Safely turn agent-written Python into a callable strategy(view, pos) -> float.
|
| 2 |
+
|
| 3 |
+
In-process restricted exec (NOT a per-rollout sandbox — the hackathon docs warn that
|
| 4 |
+
per-rollout sandboxes burn credits fast). The strategy surface is deliberately tiny:
|
| 5 |
+
a pure function over a feature dict + current position. No imports, no IO, no builtins
|
| 6 |
+
beyond a safe math allowlist. A strategy that tries to escape returns an error string,
|
| 7 |
+
which the rubric maps to reward 0.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import math
|
| 13 |
+
from typing import Callable
|
| 14 |
+
|
| 15 |
+
_SAFE_BUILTINS = {
|
| 16 |
+
"abs": abs, "min": min, "max": max, "round": round, "float": float,
|
| 17 |
+
"int": int, "bool": bool, "len": len, "sum": sum, "pow": pow,
|
| 18 |
+
"True": True, "False": False, "None": None,
|
| 19 |
+
}
|
| 20 |
+
_SAFE_MATH = {k: getattr(math, k) for k in (
|
| 21 |
+
"sqrt", "log", "log10", "exp", "fabs", "floor", "ceil", "tanh", "isnan", "isfinite", "pi", "e"
|
| 22 |
+
)}
|
| 23 |
+
|
| 24 |
+
_FORBIDDEN = ("import", "__", "open(", "eval(", "exec(", "globals(", "locals(",
|
| 25 |
+
"compile(", "getattr(", "setattr(", "input(", "lambda os")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def compile_strategy(code: str) -> tuple[Callable | None, str | None]:
|
| 29 |
+
"""Compile agent code that must define `def strategy(features, position):`.
|
| 30 |
+
|
| 31 |
+
Returns (callable, None) on success or (None, error_message) on failure.
|
| 32 |
+
"""
|
| 33 |
+
if not code or "def strategy" not in code:
|
| 34 |
+
return None, "code must define `def strategy(features, position):`"
|
| 35 |
+
lowered = code.lower()
|
| 36 |
+
for tok in _FORBIDDEN:
|
| 37 |
+
if tok in lowered:
|
| 38 |
+
return None, f"forbidden token in strategy: {tok!r}"
|
| 39 |
+
|
| 40 |
+
sandbox_globals = {"__builtins__": _SAFE_BUILTINS, "math": _SAFE_MATH, **_SAFE_MATH}
|
| 41 |
+
try:
|
| 42 |
+
compiled = compile(code, "<strategy>", "exec")
|
| 43 |
+
exec(compiled, sandbox_globals) # noqa: S102 - restricted globals, no builtins
|
| 44 |
+
except Exception as e: # noqa: BLE001
|
| 45 |
+
return None, f"compile/exec error: {type(e).__name__}: {e}"
|
| 46 |
+
|
| 47 |
+
fn = sandbox_globals.get("strategy")
|
| 48 |
+
if not callable(fn):
|
| 49 |
+
return None, "`strategy` is not callable"
|
| 50 |
+
|
| 51 |
+
def wrapped(view: dict, pos: float) -> float:
|
| 52 |
+
return fn(view, pos)
|
| 53 |
+
|
| 54 |
+
return wrapped, None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def extract_code(completion: str) -> str:
|
| 58 |
+
"""Pull a python code block out of a model completion (```python ... ``` or raw)."""
|
| 59 |
+
if "```" in completion:
|
| 60 |
+
parts = completion.split("```")
|
| 61 |
+
for i in range(1, len(parts), 2):
|
| 62 |
+
block = parts[i]
|
| 63 |
+
if block.startswith("python"):
|
| 64 |
+
block = block[len("python"):]
|
| 65 |
+
if "def strategy" in block:
|
| 66 |
+
return block.strip()
|
| 67 |
+
return completion.strip()
|
trade_pool/features.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Causal technical indicators over an OHLCV frame.
|
| 2 |
+
|
| 3 |
+
Every function takes a 1-D array/series of closes (or OHLCV) and returns a series
|
| 4 |
+
aligned to the SAME index, where value[i] uses ONLY data at indices <= i. This is
|
| 5 |
+
the structural anti-lookahead guarantee: a strategy can request features at bar t
|
| 6 |
+
and physically cannot see t+1.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def sma(close: np.ndarray, window: int) -> np.ndarray:
|
| 15 |
+
out = np.full_like(close, np.nan, dtype=float)
|
| 16 |
+
if window <= 0:
|
| 17 |
+
return out
|
| 18 |
+
csum = np.cumsum(np.insert(close, 0, 0.0))
|
| 19 |
+
out[window - 1:] = (csum[window:] - csum[:-window]) / window
|
| 20 |
+
return out
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def ema(close: np.ndarray, window: int) -> np.ndarray:
|
| 24 |
+
out = np.full_like(close, np.nan, dtype=float)
|
| 25 |
+
if len(close) == 0 or window <= 0:
|
| 26 |
+
return out
|
| 27 |
+
alpha = 2.0 / (window + 1.0)
|
| 28 |
+
out[0] = close[0]
|
| 29 |
+
for i in range(1, len(close)):
|
| 30 |
+
out[i] = alpha * close[i] + (1 - alpha) * out[i - 1]
|
| 31 |
+
return out
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def rsi(close: np.ndarray, window: int = 14) -> np.ndarray:
|
| 35 |
+
out = np.full_like(close, np.nan, dtype=float)
|
| 36 |
+
if len(close) < window + 1:
|
| 37 |
+
return out
|
| 38 |
+
delta = np.diff(close)
|
| 39 |
+
gain = np.where(delta > 0, delta, 0.0)
|
| 40 |
+
loss = np.where(delta < 0, -delta, 0.0)
|
| 41 |
+
avg_gain = np.mean(gain[:window])
|
| 42 |
+
avg_loss = np.mean(loss[:window])
|
| 43 |
+
for i in range(window, len(close)):
|
| 44 |
+
if i > window:
|
| 45 |
+
avg_gain = (avg_gain * (window - 1) + gain[i - 1]) / window
|
| 46 |
+
avg_loss = (avg_loss * (window - 1) + loss[i - 1]) / window
|
| 47 |
+
rs = avg_gain / avg_loss if avg_loss > 1e-12 else np.inf
|
| 48 |
+
out[i] = 100.0 - (100.0 / (1.0 + rs))
|
| 49 |
+
return out
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def macd(close: np.ndarray, fast: int = 12, slow: int = 26, signal: int = 9):
|
| 53 |
+
macd_line = ema(close, fast) - ema(close, slow)
|
| 54 |
+
signal_line = ema(np.nan_to_num(macd_line), signal)
|
| 55 |
+
return macd_line, signal_line, macd_line - signal_line
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def zscore(close: np.ndarray, window: int = 20) -> np.ndarray:
|
| 59 |
+
out = np.full_like(close, np.nan, dtype=float)
|
| 60 |
+
for i in range(window - 1, len(close)):
|
| 61 |
+
w = close[i - window + 1: i + 1]
|
| 62 |
+
mu, sd = w.mean(), w.std()
|
| 63 |
+
out[i] = (close[i] - mu) / sd if sd > 1e-12 else 0.0
|
| 64 |
+
return out
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def bollinger(close: np.ndarray, window: int = 20, k: float = 2.0):
|
| 68 |
+
mid = sma(close, window)
|
| 69 |
+
out_w = np.full_like(close, np.nan, dtype=float)
|
| 70 |
+
for i in range(window - 1, len(close)):
|
| 71 |
+
out_w[i] = close[i - window + 1: i + 1].std()
|
| 72 |
+
return mid - k * out_w, mid, mid + k * out_w
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def returns(close: np.ndarray) -> np.ndarray:
|
| 76 |
+
out = np.zeros_like(close, dtype=float)
|
| 77 |
+
out[1:] = np.diff(close) / np.where(close[:-1] == 0, np.nan, close[:-1])
|
| 78 |
+
return np.nan_to_num(out)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def volatility(close: np.ndarray, window: int = 20) -> np.ndarray:
|
| 82 |
+
r = returns(close)
|
| 83 |
+
out = np.full_like(close, np.nan, dtype=float)
|
| 84 |
+
for i in range(window - 1, len(close)):
|
| 85 |
+
out[i] = r[i - window + 1: i + 1].std()
|
| 86 |
+
return out
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# The full feature surface exposed to a strategy at bar t (all causal).
|
| 90 |
+
def feature_frame(close: np.ndarray) -> dict[str, np.ndarray]:
|
| 91 |
+
macd_line, macd_signal, macd_hist = macd(close)
|
| 92 |
+
bb_lo, bb_mid, bb_hi = bollinger(close)
|
| 93 |
+
return {
|
| 94 |
+
"close": close,
|
| 95 |
+
"sma_10": sma(close, 10),
|
| 96 |
+
"sma_20": sma(close, 20),
|
| 97 |
+
"sma_50": sma(close, 50),
|
| 98 |
+
"ema_12": ema(close, 12),
|
| 99 |
+
"ema_26": ema(close, 26),
|
| 100 |
+
"rsi_14": rsi(close, 14),
|
| 101 |
+
"macd": macd_line,
|
| 102 |
+
"macd_signal": macd_signal,
|
| 103 |
+
"macd_hist": macd_hist,
|
| 104 |
+
"zscore_20": zscore(close, 20),
|
| 105 |
+
"bb_lo": bb_lo,
|
| 106 |
+
"bb_mid": bb_mid,
|
| 107 |
+
"bb_hi": bb_hi,
|
| 108 |
+
"ret_1": returns(close),
|
| 109 |
+
"vol_20": volatility(close, 20),
|
| 110 |
+
}
|
trade_pool/seed_principles.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Distil tradewatch's 618 real trading decisions into grounded principles + exemplars
|
| 2 |
+
that ground the strategy-writing prompt.
|
| 3 |
+
|
| 4 |
+
These are NOT invented discipline rules — they are mined from actual BUY/AVOID decisions
|
| 5 |
+
the tradewatch agent made on live Base-chain tokens, so the discipline the coding agent
|
| 6 |
+
encodes into strategy logic is traceable to real prior trading, not a paraphrase. The
|
| 7 |
+
distilled block is baked into the wheel (committed below as a constant) so it ships with
|
| 8 |
+
the env and needs no runtime access to the tradewatch repo.
|
| 9 |
+
|
| 10 |
+
Regenerate with: python -m trade_pool.seed_principles --rebuild (dev only)
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
# Baked-in distillation (committed). Mined from data/seed/decisions.jsonl:
|
| 19 |
+
# 94 BUY / 489 WATCH / 35 AVOID across 9 tokens, all with reasoning.
|
| 20 |
+
SEED_PRINCIPLES = """\
|
| 21 |
+
Trading discipline distilled from 618 real prior trading decisions (94 BUY, 489 WATCH, 35 AVOID):
|
| 22 |
+
|
| 23 |
+
Patterns that earned a BUY (translate these into your strategy's entry logic):
|
| 24 |
+
- "Post-dip curl": a 24h drop followed by a 1h recovery with a strong short-term buy/sell
|
| 25 |
+
ratio (>1.5). Enter on the curl, not the dip. → buy when recent return turns up after a
|
| 26 |
+
drawdown and momentum (rsi rising from oversold, macd_hist turning positive) confirms.
|
| 27 |
+
- "Healthy momentum continuation": positive 1h AND 24h change with buy pressure and
|
| 28 |
+
volume/liquidity > 1. → trend-follow when sma_10 > sma_20 and macd > macd_signal.
|
| 29 |
+
- Size up only with liquidity + volume backing the move; size down when volatility is high.
|
| 30 |
+
|
| 31 |
+
Conditions that earned WATCH/AVOID (translate into your exits and risk limits):
|
| 32 |
+
- Already +30% on the day → entry timing is poor; do not chase. → avoid full long when
|
| 33 |
+
ret_1 is extremely high or price is far above bb_mid.
|
| 34 |
+
- Volume/liquidity < 1.0 or thin liquidity → lower conviction; reduce exposure.
|
| 35 |
+
- Sell-heavy short-term flow after entry → cut quickly; don't wait for a vague recovery.
|
| 36 |
+
|
| 37 |
+
Capital-protection rules (every BUY required these):
|
| 38 |
+
- Always define a stop below entry and a target above; favor reward:risk >= 2:1.
|
| 39 |
+
- Protect capital first: control drawdown over chasing upside.
|
| 40 |
+
- Don't average down losers; don't let a winner round-trip — tighten when flow weakens.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def principles_block() -> str:
|
| 45 |
+
return SEED_PRINCIPLES
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# ── Dev-only: regenerate the constant from the raw journal extraction ──
|
| 49 |
+
def _rebuild() -> str:
|
| 50 |
+
seed = Path(__file__).resolve().parents[3] / "data" / "seed" / "decisions.jsonl"
|
| 51 |
+
rows = [json.loads(l) for l in seed.read_text().splitlines() if l.strip()]
|
| 52 |
+
buys = [r for r in rows if r.get("verdict") == "BUY"]
|
| 53 |
+
patterns = {}
|
| 54 |
+
for r in buys:
|
| 55 |
+
p = (r.get("pattern") or "unlabeled").strip()
|
| 56 |
+
patterns[p] = patterns.get(p, 0) + 1
|
| 57 |
+
print(f"{len(rows)} decisions | BUY patterns: {patterns}")
|
| 58 |
+
print("Edit SEED_PRINCIPLES by hand from these — keep it compact and strategy-actionable.")
|
| 59 |
+
return ""
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
import sys
|
| 64 |
+
if "--rebuild" in sys.argv:
|
| 65 |
+
_rebuild()
|
| 66 |
+
else:
|
| 67 |
+
print(principles_block())
|