An AI agent that taught itself to trade crypto
103 experiments, zero human intervention
392 stars · Python · Hyperliquid perpetual futures
The simple version anyone can understand
The punchline: The best changes were removing ingredients, not adding them. Simpler was better.
Claude Code (an AI coding agent) runs this loop over and over, fully autonomously:
Commit the change, record the new score, and try to improve further.
Revert the change, go back to the last good version, try something else.
Constraint: The AI can only edit one file. Everything else is locked.
What's a Sharpe ratio? It measures return vs. risk. A Sharpe of 2 is "very good" in traditional finance. 21.4 is extraordinary — though this is on historical data, not live trading.
The robot discovered that removing fancy features (pyramid trading, BTC filters, correlation weighting) made the strategy dramatically better.
The final strategy works like a panel of 6 judges voting on every trade:
"Is the price going up over the last 12 hours?"
"Is the price going up over the last 6 hours?"
"Is the short-term average above the long-term?"
"Is there buying strength right now?"
"Is momentum accelerating?"
"Is volatility compressed? (a breakout may be coming)"
The rule: At least 4 out of 6 judges must agree before any trade happens. This filters out noise.
Architecture, code patterns, scoring, and what the AI actually learned
| strategy.py | The only editable file |
| backtest.py | Fixed evaluation harness |
| prepare.py | Data download + scoring engine |
| run_benchmarks.py | 5 reference strategies |
| program.md | AI agent instructions |
Only strategy.py is modifiable. The agent cannot change the backtester, the data pipeline, or the scoring function.
This is the "Karpathy-style" research pattern: constrain the search space, automate the iteration.
numpy pandas scipy requests pyarrow
| Split | Period |
|---|---|
| Train | 2023-06 to 2024-06 |
| Validation | 2024-07 to 2025-03 |
| Test | 2025-04 to 2025-12 |
| Initial capital | $100,000 |
| Maker fee | 2 bps |
| Taker fee | 5 bps |
| Slippage | 1 bps |
| Max leverage | 20x |
| Time budget | 120 seconds |
Funding rates applied every bar (1/8th scaling since funding settles every 8h). Longs pay when positive, shorts receive.
The AI optimizes a single composite score — not just raw returns:
score = sharpe * sqrt(min(trades / 50, 1.0)) - drawdown_penalty - turnover_penalty
These prevent degenerate strategies from polluting the search.
Every strategy must implement a Strategy class with a single method:
class Strategy: def __init__(self): ... # initialize state def on_bar(self, bar_data, portfolio) -> list[Signal]: ... # return target positions
| .close | Current close price |
| .history | DataFrame of last 500 bars |
| .funding_rate | Current funding rate |
Signal( symbol="ETH", target_position=8000.0 # USD notional ) # +long, -short, 0=flat
Signals are target positions, not deltas. The backtester handles execution, fees, and slippage.
The winning strategy uses a majority-vote ensemble requiring 4/6 agreement:
ret_short > dyn_threshold
12-bar return vs volatility-adjusted threshold (0.5% – 2%)
ret_vshort > thresh * 0.7
Shorter lookback at 70% threshold for responsiveness
ema_fast > ema_slow
Fast EMA (7) crossing above slow EMA (26)
rsi > 50 (bull) / < 50 (bear)
Key finding: period 8 >>> 14 for hourly crypto
macd_histogram > 0
Non-standard params tuned for hourly timeframe
bb_pctile < 90
Bollinger width below 90th pctile = pending breakout
Entry signals use a dynamic threshold that adapts to current market volatility:
realized_vol = std(log_returns[-36:]) vol_ratio = realized_vol / 0.015 # target vol dyn_threshold = 0.012 * (0.3 + vol_ratio * 0.7) dyn_threshold = clamp(0.005, 0.020) # floor/ceiling
| VOL_LOOKBACK | 36 bars (1.5 days) |
| TARGET_VOL | 0.015 (1.5%/hr) |
| BASE_THRESHOLD | 0.012 (1.2%) |
| Range | 0.5% – 2.0% |
Three independent exit triggers, any one closes the position:
Tracks peak price since entry. Exits when price drops 5.5 ATR units from peak. The wide multiplier gives trades room to breathe.
peak = max(peak, current_price) stop = peak - 5.5 * atr if price < stop: exit()
Long exits at RSI > 69 (overbought). Short exits at RSI < 31 (oversold). These catch exhaustion points.
If the ensemble flips (4/6 bearish while long, or 4/6 bullish while short), the position reverses immediately. No waiting.
2-bar cooldown after every exit prevents rapid-fire re-entry. Reduces churning and noise trading.
8% of equity per trade, split equally across BTC/ETH/SOL (33% weight each).
Defined in program.md — instructions the AI agent follows:
Key insight: The agent reads the full history of past experiments before each attempt. It learns from its own failures — like having an ever-growing lab notebook. The 120-second time budget per backtest prevents runaway compute.
103 experiments across 7 distinct phases of discovery:
Added ensemble voting. Requiring multiple signals to agree was the single biggest breakthrough.
Widened ATR trailing stops from 3.0x to 5.5x. Gave winning trades more room.
Added BB width compression as a 6th signal — detects pending breakouts.
Removed pyramiding, funding boosts, BTC filters, correlation adjustments, and strength scaling. +4.5 points from subtraction alone.
RSI period 8 (was 14). This single change added 5 Sharpe points. MACD tuned to 14/23/9.
Removing complexity improved performance.
Each "sophisticated" feature was tested — and hurt the score:
| Feature Removed | Impact on Score | Why It Hurt |
|---|---|---|
| Strength-scaled sizing | +1.7 | Overfit to magnitude of signals |
| BTC lead-lag filter | +0.8 | Added lag, missed entries |
| Pyramiding (adding to winners) | +0.6 | Increased drawdown on reversals |
| Funding rate carry boost | +0.5 | Noise in short-term funding |
| Correlation weighting | +0.4 | Regime-dependent, unreliable |
| Drawdown risk reduction | +0.5 | Drawdown so low it never triggered |
Total gain from simplification: +4.5 Sharpe points. The "dead code" approach: set thresholds to unreachable values (99.0, -99.0) to neutralize features.
The single most impactful parameter change in the entire research:
Score: 14.8
Score: 19.7
+5.0 Sharpe points from changing one number.
This is a general lesson: technical indicator defaults are calibrated for daily equities. Always re-tune for your timeframe and asset class.
The core execution path each bar (simplified):
def on_bar(self, bar_data, portfolio): signals = [] for symbol in ["BTC", "ETH", "SOL"]: closes = bar_data[symbol].history["close"].values # 1. Compute dynamic threshold from realized vol dyn_threshold = calc_vol_threshold(closes) # 2. Calculate 6 signals mom_bull = ret_12h(closes) > dyn_threshold vshort_bull = ret_6h(closes) > dyn_threshold * 0.7 ema_bull = ema(7) > ema(26) rsi_bull = rsi(8) > 50 macd_bull = macd_hist(14,23,9) > 0 bb_bull = bb_width_pctile(7) < 90 # 3. Majority vote (4 of 6) bull_votes = sum([mom, vshort, ema, rsi, macd, bb]) if bull_votes >= 4: go_long() # 4. Exit: ATR trail | RSI extreme | signal reversal if price < peak - 5.5*atr: exit() if rsi > 69 and is_long(): exit() return signals
Full implementation: ~250 lines of Python. No external models, no ML — pure signal processing.
5 reference strategies are provided in benchmarks/ for comparison:
| Strategy | Type | Description |
|---|---|---|
| momentum_breakout | Trend | The original baseline (Sharpe 2.7) |
| mean_reversion | Mean Revert | Fade extreme moves, bet on return to mean |
| avellaneda_mm | Market Making | Avellaneda-Stoikov optimal market making |
| regime_mm | Adaptive MM | Regime-switching market maker |
| funding_arb | Carry | Exploit funding rate premiums |
The leaderboard runner (run_benchmarks.py) loads each strategy, backtests on val data, and ranks by composite score. The autonomous agent's final strategy outperforms all five by 7.9x+.
| Milestone | Score | Key Change |
|---|---|---|
| Baseline | 2.7 | Simple momentum |
| Ensemble | 8.4 | 4/6 majority vote |
| + BB width | 10.3 | 6th signal added |
| Simplified | 14.8 | Removed 6 features |
| Final | 21.4 | RSI(8) + param tuning |
The value is in the process, not the final Sharpe number.
The strategy file contains "dead" features neutralized by setting params to unreachable values:
# These features exist in code but are effectively disabled: FUNDING_BOOST = 0.0 # multiplier = 1.0, no effect TAKE_PROFIT_PCT = 99.0 # never triggers at 99x BTC_OPPOSE_THRESHOLD = -99.0 # BTC filter effectively off PYRAMID_SIZE = 0.0 # adds 0 to position HIGH_CORR_THRESHOLD = 99.0 # correlation never this high DD_REDUCE_THRESHOLD = 99.0 # drawdown never reaches 99%
Why not delete the code? The AI agent keeps the infrastructure so it can re-enable features in future experiments. Setting extreme thresholds is a reversible way to test the effect of removing a feature without losing the implementation.
This is a fascinating artifact of autonomous code evolution — code that documents its own failed hypotheses.
The meta-lesson: An AI agent with 103 tries and a good scoring function can discover what a human quant might take weeks to find — and the biggest discovery was removing complexity, not adding it.
| Repository | github.com/Nunchi-trade/auto-researchtrading |
| Stars | 392 (and growing) |
| Language | Python 3.10+ (numpy, pandas, scipy) |
| Quick start | uv run prepare.py && uv run backtest.py |
| Full log | STRATEGIES.md — all 103 experiments |
| AI prompt | program.md — the autonomous loop instructions |
agent37.club