/** * Strategy cookbook: trend, a moving-average crossover. A drop-in strategy.ts 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.ts and keep "dry_run": true in config.json until the log * shows what you expect. decide(market, book, state) gets what the starter bot's strategy.ts gets (prices, the wallet's * balances, and state kept in bot-state.json) and returns a list of orders. bot.ts 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. */ export type Market = Record export type Holding = { units: number | null; usd: number | null } export type Book = { balances: Record; totalUsd: number } export type State = Record export type Order = { sell: string // the token to sell, e.g. 'ETH' buy: string // the token to buy, e.g. 'USDG' amountUsd: number // how many dollars of `sell` to trade reason?: string // why: shown in the console and bot-log.jsonl } const FAST = 10 // ticks in the fast average (with interval_s = 60, the last 10 minutes) const SLOW = 30 // ticks in the slow average (the last 30 minutes): longer averages cross less often const BAND = 0.002 // the averages must be 0.2% apart to count as a cross: a wider band trades less in sideways prices const TRADE_USD = 10 // dollars to trade on each cross const MAX_GAP_S = 3600 // a tick more than an hour after the last one (the bot was stopped) starts the prices again export function decide(market: Market, book: Book, state: State): Order[] { let s = (state.trend ??= {}) // this strategy's own corner of state const price = market.ETH if (price === undefined) return [] // no ETH price this tick: wait for the next one const now = Date.now() / 1000 if (now - (s.at ?? now) > MAX_GAP_S) s = state.trend = {} // stopped for a while: old prices would mislead the averages s.at = now const prices: number[] = (s.prices ??= []) prices.push(price) if (prices.length > SLOW) prices.splice(0, prices.length - SLOW) // keep only the last SLOW prices if (prices.length < SLOW) return [] // still collecting prices const fast = sum(prices.slice(-FAST)) / FAST const slow = sum(prices) / SLOW let side: 'above' | 'below' | 'between' if (fast > slow * (1 + BAND)) side = 'above' else if (fast < slow * (1 - BAND)) side = 'below' else side = 'between' // within the band: too close to call const before = s.side if (before === undefined || side !== 'between') s.side = side // inside the band, the last side stands if (before === undefined || side === 'between' || side === before) return [] // the first reading, or no cross const why = `the ${FAST}-tick average $${fast.toFixed(2)} crossed ${side} the ${SLOW}-tick average $${slow.toFixed(2)}` if (side === 'above') return [{ sell: 'USDG', buy: 'ETH', amountUsd: TRADE_USD, reason: why }] return [{ sell: 'ETH', buy: 'USDG', amountUsd: TRADE_USD, reason: why }] } const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0)