/** * Your strategy: the only file you need to edit. * * Every tick, bot.ts reads prices and the wallet's balances, then calls decide(market, book, state). decide returns the * orders it wants: a list of Order, or an empty list to do nothing. bot.ts then checks each order against your limits in * config.json, and trades it (or, with dry_run on, only checks what the trade would do). * * Keep this file free of network calls, keys and sending: bot.ts does all of that, inside the limits you set. * * What decide gets: * * market USD price of each token in config.json's `tokens`, read from Chainlink on chain by the SDK: * { ETH: 2512.37, USDG: 1 }. USDG is $1. A token without a price feed is left out. * * book the wallet's balances: * { balances: { ETH: { units: 0.0398, usd: 100.0 }, * USDG: { units: 102.5, usd: 102.5 } }, * totalUsd: 202.5 } * `usd` is null for a token without a price, and totalUsd adds up only the priced ones. * * state an object that persists between ticks, and between runs (bot.ts saves it to bot-state.json). Keep what you * need in it: a running high, the last price, a counter. Use plain values (numbers, strings, arrays, objects). * * What decide returns: a list of { sell, buy, amountUsd, reason }. `sell` and `buy` are names from `tokens` (e.g. 'ETH', * 'USDG'), `amountUsd` is how many dollars of `sell` to trade, and `reason` is shown in the log. * * You don't have to size orders carefully: bot.ts cuts every order to max_trade_usd, to what is left of max_day_usd, * and to what the wallet holds, and skips anything under $1. */ 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 } // --- Example 1 (the one that runs): keep a target split between ETH and USDG ----------------------------------------- // // The wallet should hold TARGET_ETH of its value in ETH and the rest in USDG. When ETH drifts more than BAND away from // that (it went up, so ETH is now too large a share; or it fell), the strategy trades back toward the target. // A wider band trades less often. const TARGET_ETH = 0.5 // 50% of the wallet's value in ETH, 50% in USDG const BAND = 0.05 // do nothing while ETH is within 45%..55% const MIN_BOOK_USD = 5 // too little to bother with below this export function decide(market: Market, book: Book, state: State): Order[] { state.ticks = (state.ticks ?? 0) + 1 // an example of state: how many ticks this bot has run // ---------------------------------------------------------------------------------------------------------------- // YOUR LOGIC GOES HERE. Replace the lines below with your own, or switch to Example 2 at the bottom of this file: // return decideDipBuy(market, book, state) // ---------------------------------------------------------------------------------------------------------------- const price = market.ETH if (price === undefined) return [] // no ETH price this tick: wait for the next one const total = book.totalUsd if (total < MIN_BOOK_USD) return [] const ethUsd = book.balances.ETH?.usd ?? 0 const share = ethUsd / total state.lastEthShare = Math.round(share * 10_000) / 10_000 const drift = share - TARGET_ETH if (Math.abs(drift) <= BAND) return [] // inside the band: nothing to do const usd = Math.abs(drift) * total // the dollars that bring ETH back to its target const why = `ETH is ${pct(share)} of $${total.toFixed(2)}; the target is ${pct(TARGET_ETH)}` if (drift > 0) return [{ sell: 'ETH', buy: 'USDG', amountUsd: usd, reason: why }] return [{ sell: 'USDG', buy: 'ETH', amountUsd: usd, reason: why }] } // --- Example 2: buy the dip ------------------------------------------------------------------------------------------ // // Remembers the highest ETH price it has seen (in state). When ETH falls DIP below that high, it buys BUY_USD of ETH // with USDG and starts measuring from the new, lower price. To use it, make decide() return // decideDipBuy(market, book, state). const DIP = 0.03 // buy after a 3% fall from the high const BUY_USD = 5 // dollars of USDG to spend on each dip export function decideDipBuy(market: Market, book: Book, state: State): Order[] { const price = market.ETH if (price === undefined) return [] const high = Math.max(state.high ?? price, price) state.high = high if (price > high * (1 - DIP)) return [] // not down enough yet const usdg = book.balances.USDG?.usd ?? 0 if (usdg < BUY_USD) return [] // nothing left to buy with state.high = price // the next dip is measured from here return [{ sell: 'USDG', buy: 'ETH', amountUsd: BUY_USD, reason: `ETH $${price.toFixed(2)} is ${pct(1 - price / high)} under its high of $${high.toFixed(2)}` }] } const pct = (x: number) => `${(x * 100).toFixed(0)}%`