# The Monkey Test

Does a trading rule time its entries better than random entries? This is the code behind the ProEA Lab video "THE MONKEY
TEST": six popular rules on Binance spot 4-hour candles (BTC, ETH, SOL, BNB, LTC, XRP), each set against 1,000 "monkeys".

A monkey makes exactly as many trades as the rule and holds them exactly as long (the holding times shuffled), but places
them at random, without overlap, in the same window. Only the timing differs. A rule that beats most monkeys times its
entries better than chance; a rule that loses to most of them does not.

A simulation on real prices. Research tooling, not financial advice. Backtests are not future results.

## Files

| File | What it does |
|---|---|
| `fetch.py` | Downloads every 4h candle for one symbol from Binance's public market-data mirror (data-api.binance.vision). |
| `monkey.py` | The test: six rules × six coins, 1,000 monkeys each, one table per coin. |
| `fun.py` | For one coin: $10,000 curves, drawdowns, the luckiest monkey, crash windows and a yearly race. |

These are the exact files the video's figures came from. `monkey.py`'s own header still names the folders the film used
(`tools/`, `data/`); here everything sits in one folder.

## Run it

You need Python 3 with numpy and pandas. A virtual environment avoids "externally managed environment" errors:

```bash
python3 -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate
pip install numpy pandas
```

Then, in the folder with the three files. `monkey.py` reads all six coins, so fetch all six:

```bash
mkdir data
python3 fetch.py BTCUSDT 4h data/BTCUSDT_4h.json.gz
python3 fetch.py ETHUSDT 4h data/ETHUSDT_4h.json.gz
python3 fetch.py SOLUSDT 4h data/SOLUSDT_4h.json.gz
python3 fetch.py BNBUSDT 4h data/BNBUSDT_4h.json.gz
python3 fetch.py LTCUSDT 4h data/LTCUSDT_4h.json.gz
python3 fetch.py XRPUSDT 4h data/XRPUSDT_4h.json.gz
python3 monkey.py data 0.001                             # 0.1% fee per side, the video's window
python3 monkey.py data 0.001 2022-01-01 2026-10-25       # only the later years
python3 fun.py data BTCUSDT
```

`monkey.py data FEE FROM TO` takes the fee per side and the window. With the defaults it reproduces the video's tables:
we re-ran these files on candles fetched after the video and got the same output line for line.

`fun.py` needs a coin with candles from before March 2020 (its crash windows start with COVID), so it runs for BTC, ETH,
BNB, LTC and XRP but not SOL.

## The rules of the test

- Long only, one position at a time, no stop-loss, no leverage.
- A signal on a bar's close is filled at the next bar's open. The fee is charged on every buy and every sell.
- The window runs from 1 Jan 2018, or from a coin's first Binance candle if that is later (XRP May 2018, SOL Aug 2020),
  to the last closed bar before 25 Oct 2026.
- Indicators follow TradingView's formulas (EMA, Wilder's RSI, MACD 12/26/9, `ta.supertrend`) and warm up on whatever
  bars exist before the window. Two differences: a coin with little history before the window (SOL, XRP, LTC) starts with
  the long averages still settling, and the EMA here gives a value from the first bar where TradingView's `ta.ema` waits
  for a full length.
- 1,000 monkeys per rule and coin, random seed 7, so every run gives the same numbers.

## Test your own rule

`monkey.run()` takes a function that returns your own entry and exit signals. Save this as `my_rule.py` next to
`monkey.py` and run `python3 my_rule.py` from that folder (fetch the coin first):

```python
import sys
sys.argv = ["monkey.py", "data", "0.001"]          # data folder, fee per side
import numpy as np, pandas as pd, monkey

def my_rules(df):                                   # df: the coin's full 4h history, columns o h l c v, plain row index
    c = df["c"].values
    fast = pd.Series(c).ewm(span=20, adjust=False).mean().values
    slow = pd.Series(c).ewm(span=100, adjust=False).mean().values
    return {"EMA 20/100": (fast > slow, fast < slow)}  # entry, exit (boolean arrays, one value per bar)

r = monkey.run("BTCUSDT", rules=my_rules)["res"]["EMA 20/100"]
print(f"{r['trades']} trades, x{r['x']:.2f} after fees, beats {r['beats']:.0%} of 1,000 monkeys")
```

Each signal must use only data up to that bar's close: a rule that peeks at later bars will look far better than it is.
Your rule must make at least one trade in the window, or the test has nothing to compare.

## Limits

The coins tested are ones that survived to 2026. The ones that died are not here. One exchange, spot prices, no
funding, no slippage beyond the fee. Beating the monkeys measures timing, not money: a rule can beat most of them and
still lose money after fees.

From [proea.app/en/prompts](https://proea.app/en/prompts#monkey-test).
