Strategy cookbook
Five ready-made strategies for the starter bot. Each one is a complete strategy file, in Python and TypeScript, that replaces the one the bot ships with: copy it over, keep the dry run on, and read the log. They're short examples to learn from and change. None of them predicts the price, and none is advice: any of them can lose money.
Last updated: 12 October 2026
Download#
| Strategy | Python | TypeScript | What it does |
|---|---|---|---|
| DCA | dca.py | dca.ts | buys a fixed dollar amount of ETH on a timer |
| Trend | trend.py | trend.ts | trades when a fast moving average crosses a slow one |
| Mean reversion | mean_reversion.py | mean-reversion.ts | buys a dip under the rolling mean, sells at the mean or at a stop |
| Grid | grid.py | grid.ts | buys and sells at fixed price levels around a centre |
| Take-profit and stop-loss | take_profit_stop_loss.py | take-profit-stop-loss.ts | sells the wallet's ETH at a gain or a loss from its entry price |
| README.txt | README.txt | a short how-to |
The code is under the MIT License (LICENSE.txt), as the starter bot is.
Swap one in#
Set up the starter bot first, as its quick start says. Then download a strategy into the bot's folder and copy it over the bot's own strategy file (on Windows, copy instead of cp):
cp dca.py strategy.py
python bot.py # a dry runcp dca.ts strategy.ts
npm run bot # a dry run- Keep
dry_run = trueinconfig.toml("dry_run": trueinconfig.json). Each order is checked and logged asdry, and nothing is sent. - Read
bot-log.jsonl: every tick, order and skip is there, with the strategy's reason. - Change the constants at the top of the file to suit you, and run the dry run again.
Before you go live, delete bot-state.json if you want the strategy to start afresh: what it remembers from the dry run (DCA's last buy, the grid's level, an exit that has started) carries over.
What they share#
- The same function. Each defines
decide(market, book, state)and theOrdertype exactly as the starter bot's strategy file does, so the bot runs it unchanged. - ETH and USDG. They trade only the tokens in the starter bot's default config.
- The bot's limits still apply. The bot cuts every order to
max_trade_usd($10 by default), to what is left ofmax_day_usd($40 in any 24 hours), and to 98% of what the wallet holds, and skips anything under $1. A skipped order is logged with the limit that stopped it. - They can't see what happened to an order. A strategy records what it asked for, not what was filled: a cut or skipped order, or a dry run, looks the same to it.
- Their edges. Without an ETH price they do nothing that tick. Each keeps its memory under its own name in
bot-state.json, so a restart picks up where it left off, and another strategy's leftovers can't confuse it. - No network calls, keys or sending. The bot does all of that, inside your limits.
DCA#
Buys BUY_USD of ETH with USDG every EVERY_HOURS hours, whatever the price. The first buy comes on the first tick with a price.
last = s.get("last_buy")
if last is not None and now - last < EVERY_HOURS * 3600:
return [] # not time yet
if usd_of(book, "USDG") < BUY_USD:
return [] # not enough USDG: nothing is recorded, so it tries next tick
s["last_buy"] = now
return [Order(sell="USDG", buy="ETH", amount_usd=BUY_USD, ...)]| Constant | Default | What it sets |
|---|---|---|
BUY_USD | 5 | dollars of USDG spent on each buy |
EVERY_HOURS | 24 | hours between buys |
When it trades. Once every EVERY_HOURS: a smaller value trades more often. It doesn't catch up on buys it missed: after the bot was stopped for three days it buys once, then waits EVERY_HOURS again. With less than BUY_USD of USDG it waits, and buys on the first tick after the wallet is funded. The time of the last buy is kept in state, so a restart doesn't buy again early.
Trend#
A moving-average crossover. Each tick it adds ETH's price to a list in state (the last SLOW prices) and compares two averages: the FAST most recent prices and all SLOW of them. When the fast average crosses above the slow one it buys TRADE_USD of ETH; when it crosses below, it sells TRADE_USD of ETH.
fast = sum(prices[-FAST:]) / FAST
slow = sum(prices) / SLOW
if fast > slow * (1 + BAND):
side = "above"
elif fast < slow * (1 - BAND):
side = "below"
else:
side = "between" # within the band: too close to call| Constant | Default | What it sets |
|---|---|---|
FAST | 10 | ticks in the fast average (10 minutes at interval_s = 60) |
SLOW | 30 | ticks in the slow average, and how many prices it keeps |
BAND | 0.002 | how far apart the averages must be to count as a cross (0.2%) |
TRADE_USD | 10 | dollars traded on each cross |
MAX_GAP_S | 3600 | a tick this long after the last one starts the prices again |
When it trades. Only on a cross it saw happen: it trades nothing for the first SLOW ticks, and the first reading after that only notes the side. A steady rise or fall gives one trade; prices that go sideways can cross back and forth. A wider BAND or a longer SLOW trades less often. After the bot was stopped for more than MAX_GAP_S, it collects SLOW fresh prices before it trades again, so old prices don't decide. Keep interval_s well under MAX_GAP_S.
Mean reversion with a stop#
Keeps the last WINDOW prices and their mean. When ETH falls DIP under the mean it buys BUY_USD of ETH and remembers what it bought. It sells that ETH when the price is back at the mean, or when it falls STOP under the price it paid.
if held:
if price <= held["price"] * (1 - STOP):
s["held"], s["wait"] = None, True # stopped out: no new dip until ETH is back at the mean
return [sell(held, price, ...)]
if mean is not None and price >= mean:
s["held"] = None
return [sell(held, price, ...)]
return [] # holding: neither the mean nor the stop yet| Constant | Default | What it sets |
|---|---|---|
WINDOW | 30 | ticks in the rolling mean |
DIP | 0.02 | how far under the mean a dip is (2%) |
STOP | 0.03 | how far under its buy price it sells (3%) |
BUY_USD | 10 | dollars of USDG spent on each dip |
MAX_GAP_S | 3600 | a tick this long after the last one starts the mean again |
When it trades. It holds one position at a time. A deeper DIP or a longer WINDOW buys less often. A price that keeps falling reaches the stop, not the mean, and after a stop it buys no new dip until the price is back at the mean. With less than BUY_USD of USDG it records no position. A sale is the ETH it bought at today's price, so the bot cuts a sale over max_trade_usd to that limit. After a long stop the mean starts again, and a position it holds stays.
Grid#
Lays price levels STEP apart around a centre: LEVELS above and LEVELS below. Each time ETH falls through a level it buys ORDER_USD of ETH, and each time it rises through one it sells ORDER_USD of ETH.
while level > -LEVELS and price <= centre * (1 + (level - 1) * STEP):
level -= 1 # down through a level: buy
while level < LEVELS and price >= centre * (1 + (level + 1) * STEP):
level += 1 # up through a level: sell| Constant | Default | What it sets |
|---|---|---|
CENTRE | 0 | the grid's middle in dollars; 0 uses the ETH price on the grid's first tick |
STEP | 0.02 | the gap between levels (2% of the centre) |
LEVELS | 3 | levels on each side of the centre |
ORDER_USD | 5 | dollars traded at each level |
When it trades. It remembers the level it last traded at, so a sale needs the price a whole step above the last buy, and a price that wobbles around one level doesn't trade every tick. Wider steps trade less often. A jump through several levels in one tick trades them all in one order, which the bot may cut to max_trade_usd. Under the lowest level it buys no more, and over the highest it sells no more. The grid follows the price, not the wallet: when the wallet can't pay for a level, the bot skips that order and the grid still moves. Changing CENTRE starts a new grid.
Take-profit and stop-loss#
Watches the ETH the wallet holds, from an entry price. When ETH rises TAKE_PROFIT above it, or falls STOP_LOSS under it, the strategy sells the whole position for USDG. It never buys.
if "exit" not in s:
if price >= entry * (1 + TAKE_PROFIT):
s["exit"] = f"take-profit: ..."
elif price <= entry * (1 - STOP_LOSS):
s["exit"] = f"stop-loss: ..."
else:
return [] # between the two: hold
return [Order(sell="ETH", buy="USDG", amount_usd=eth_usd, reason=s["exit"])]| Constant | Default | What it sets |
|---|---|---|
TAKE_PROFIT | 0.10 | sells when ETH is this far above the entry (10%) |
STOP_LOSS | 0.05 | sells when ETH is this far under the entry (5%) |
ENTRY_PRICE | 0 | the entry price in dollars; 0 uses the ETH price on the first tick that sees the position |
MIN_POSITION_USD | 2 | less ETH than this, in dollars, is no position |
When it trades. Only once a level is reached; wider levels trade less often. The bot cuts each sale to max_trade_usd and the day's limit, so a large position is sold over several ticks: once an exit has started it carries on until less than MIN_POSITION_USD of ETH is left. Then the position is over, and the next ETH the wallet holds starts a new one at that tick's price. A price can jump past a level between ticks: the sale happens at the price of the tick that sees it. In a dry run nothing is sold, so an exit asks again every tick until the day's limit is used.
How they were tested#
Each Python strategy ran as the starter bot's strategy.py through the bot's own loop and limits, on scripted ETH prices (a steady rise, a steady fall, prices going sideways, and a sudden gap) and at its edges: no price yet, an empty wallet, a restart from saved state, and a long stop. The TypeScript versions ran through the TypeScript bot on the same prices and placed the same orders. The DCA and grid strategies also ran in the Python bot's dry run against a local copy of Robinhood Chain, with a test wallet. Those prices were made up for the tests: they show when a strategy trades, not how it would have done.
FAQ#
Which one should I use?#
That's your decision: none of them is a recommendation. Each shows a different way to decide, and any of them can lose money. Run a few as dry runs and read what they would have done.
Why didn't it trade?#
Read the log. A skip line names the limit that stopped the order (the trade's limit, the day's, or the wallet's balance), and a hold line means the strategy asked for nothing that tick.
Can I trade another token?#
Change "ETH" in the strategy to the token, and add the token to tokens in the config. In session mode, the session must be granted for it too. Read Tokens and risks first.
Is this financial advice?#
No. It's example code: you bear the outcome of every trade it makes. See the Terms of Use.