"""Strategy cookbook: trend, a moving-average crossover. A drop-in strategy.py for the Orblivion starter bot. It keeps the last SLOW ETH prices in state, one per tick, 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 with USDG; when it crosses below, it sells TRADE_USD of ETH. Averages within BAND of each other are too close to call and leave the last side standing. The first reading after the prices fill up only notes the side, so it trades only on a cross it saw happen. To use it, copy this file over the starter bot's strategy.py and keep dry_run = true in config.toml until the log shows what you expect. decide(market, book, state) gets what the starter bot's strategy.py gets (prices, the wallet's balances, and state kept in bot-state.json) and returns a list of Order. bot.py cuts every order to your limits. An example to learn from, not advice: a crossover often trades late, and back and forth when the price goes sideways. """ import time from dataclasses import dataclass @dataclass class Order: sell: str # the token to sell, e.g. "ETH" buy: str # the token to buy, e.g. "USDG" amount_usd: float # how many dollars of `sell` to trade reason: str = "" # why: shown in the console and bot-log.jsonl FAST = 10 # ticks in the fast average (with interval_s = 60, the last 10 minutes) SLOW = 30 # ticks in the slow average (the last 30 minutes): longer averages cross less often BAND = 0.002 # the averages must be 0.2% apart to count as a cross: a wider band trades less in sideways prices TRADE_USD = 10 # dollars to trade on each cross MAX_GAP_S = 3600 # a tick more than an hour after the last one (the bot was stopped) starts the prices again def decide(market, book, state): s = state.setdefault("trend", {}) # this strategy's own corner of state price = market.get("ETH") if price is None: return [] # no ETH price this tick: wait for the next one now = time.time() if now - s.get("at", now) > MAX_GAP_S: s.clear() # stopped for a while: old prices would mislead the averages s["at"] = now prices = s.setdefault("prices", []) prices.append(price) del prices[:-SLOW] # keep only the last SLOW prices if len(prices) < SLOW: return [] # still collecting prices 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 before = s.get("side") if before is None or side != "between": s["side"] = side # inside the band, the last side stands if before is None or side in ("between", before): return [] # the first reading, or no cross why = f"the {FAST}-tick average ${fast:,.2f} crossed {side} the {SLOW}-tick average ${slow:,.2f}" if side == "above": return [Order(sell="USDG", buy="ETH", amount_usd=TRADE_USD, reason=why)] return [Order(sell="ETH", buy="USDG", amount_usd=TRADE_USD, reason=why)]