Backtest a trading strategy
Read one ETH price per day from Chainlink, write buy and sell rules, and compare them with buying and holding.
Goal
Test a simple trading rule on real ETH prices:
- Buy ETH when the price is above its 20-day average.
- Sell ETH when the price falls below it.
Then check whether the rule did better than buying ETH once and holding it.
You will read the prices from the Chainlink ETH/USD price feed on Ethereum,
then let backtest_from_signals play
the trades for you. Each strategy starts with 1,000 USDC and pays a 0.30% fee
on every trade.
Before you begin
The notebook reads old blocks, so it needs an Ethereum client that keeps history (an archive node).
Each step shows a saved result. Select Run all to fetch fresh data. The steps build on each other: each one reads the table the step before it made.
1. Pick one block per day
You cannot ask the price feed for “the price on 3 July”. You can only ask what it said at a given block. So first, turn each day into a block.
block_at returns the last block at or before a
time. This step asks for the block at 00:00 UTC on each day from 1 July to
28 September 2026: 90 days.
2. Read the price on each day
The ETH/USD feed lives at 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419.
read_contract calls two of its functions
at each day’s block:
latestRoundDatareturns the price in theanswerfield.decimalssays how many digits ofanswercome after the decimal point. For ETH/USD it is 8, so an answer of250000000000means 2,500 USD.
The block number goes right after the function name. Keep answer as a whole
number: the backtest needs exact values. Use
format_units only to show the price.
3. Write the buy and sell rules
A rule is a true or false value on each day:
entryis true on days you want to buy.exitis true on days you want to sell.
This step makes two strategies:
ma20buys when the price is above the average of the last 20 days, and sells when it is below.ROWS 19 PRECEDINGmeans “this day and the 19 days before it”. The rule waits until it has 20 days of prices.holdsays buy on the first day and never says sell. It is the baseline to beat.
A rule may only look at today and earlier days. lag and PRECEDING are
safe. lead, FOLLOWING and an average over all days (OVER ()) peek at
future prices. A rule that peeks looks better in a backtest than it could
ever do for real.
4. Run the backtest
backtest_from_signals takes a query
with one row per strategy and day. It needs these columns:
| Column | What it holds |
|---|---|
strategy |
The strategy’s name. Each one gets its own cash. |
ts |
The time of the row. |
price |
The price, as a whole number. |
entry, exit |
The rules from step 3. |
init_cash |
Starting cash: 1,000 USDC is parse_units('1000', 6). |
fee_bps |
Fee per trade, in hundredths of a percent. 30 means 0.30%. |
base_decimals |
Decimals of the asset you buy. ETH has 18. |
quote_decimals |
Decimals of your cash. USDC has 6. |
price_decimals |
Decimals of the price. The feed said 8. |
A signal trades on the next day, at the next day’s price. You decide after
you see today’s price, so you can only trade after that. This is why hold
buys on 2 July, and why a signal on the last day never trades.
The feed prices ETH in US dollars. This guide counts 1 USD as 1 USDC.
5. List the trades
The ledger has one row per strategy and day. Most days have no trade. A row
with a trade has side set to buy or sell. The amounts are whole numbers
in each token’s smallest unit, so format_units makes them readable.
6. Compare the results
equity is what a strategy is worth on that day: its cash plus its ETH at
that day’s price. The value on the last day is the final result, and
arg_max(equity, ts) picks it.
exact_change turns the start and end values
into an exact percent return. Do not subtract the raw amounts yourself: they
are UINT256, a type with no negative numbers. A loss would wrap around to a
huge number. If you need the difference, cast both to INT256 first.
In the saved result, holding wins. ETH rose from about 1,567 USD to about
2,685 USD, so hold ends near 1,662 USDC, up 66%. ma20 ends near 1,158
USDC, up 16%. It waited 20 days before its first buy, sold during each dip,
bought back higher, and paid fees on 9 trades. Its last buy is still open, so
its ETH is valued at the last day’s price.
One window proves little. A rule that loses here can win in a falling market. Try other dates before you trust a result.
What to try next
- Change
ROWS 19 PRECEDINGanddays_seen = 20to try a shorter or longer average. Add it as a third strategy to compare all of them in one run. - Change
fee_bpsto see how much fees cost a strategy that trades often. - Use another feed, such as BTC/USD at
0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c. Read itsdecimals: not every feed uses 8.
Limits
- A trade buys with all the cash or sells the whole position. There is no short selling.
- Gas, price impact and failed trades are not counted.
- A position still open on the last day stays open. It is valued at the last price.