/** * Strategy cookbook: take-profit and stop-loss on a position. A drop-in strategy.ts for the Orblivion starter bot. * * The position is the ETH the wallet holds. Its entry price is ENTRY_PRICE, or with 0 the ETH price on the first tick * that sees the position (kept in state). When ETH rises TAKE_PROFIT above the entry, or falls STOP_LOSS under it, the * strategy sells the whole position for USDG. bot.ts 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. * * It never buys: fund the wallet with the ETH you want it to watch. * * 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. * In a dry run nothing is sold, so an exit asks again every tick until the day's limit is used. * * An example to learn from, not advice: a price can jump past a level between two ticks, and the sale happens at the * price of the tick that sees it. */ 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 TAKE_PROFIT = 0.1 // sell when ETH is 10% above the entry price const STOP_LOSS = 0.05 // sell when ETH is 5% under the entry price const ENTRY_PRICE = 0 // the entry price in dollars per ETH; 0: the ETH price on the first tick that sees the position const MIN_POSITION_USD = 2 // less ETH than this (dollars) is no position: what is left after a sale export function decide(market: Market, book: Book, state: State): Order[] { let s = (state.takeProfitStopLoss ??= {}) // 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 ethUsd = usdOf(book, 'ETH') if (ethUsd < MIN_POSITION_USD) { state.takeProfitStopLoss = {} // no position, or it has been sold: the next one starts afresh return [] } if (s.entry === undefined || (ENTRY_PRICE && s.entry !== ENTRY_PRICE)) { s = state.takeProfitStopLoss = { entry: ENTRY_PRICE || price } // a new position (or ENTRY_PRICE changed): its entry price } const entry: number = s.entry if (s.exit === undefined) { if (price >= entry * (1 + TAKE_PROFIT)) s.exit = `take-profit: ETH $${price.toFixed(2)} is ${pct(price / entry - 1)} above the entry $${entry.toFixed(2)}` else if (price <= entry * (1 - STOP_LOSS)) s.exit = `stop-loss: ETH $${price.toFixed(2)} is ${pct(1 - price / entry)} under the entry $${entry.toFixed(2)}` else return [] // between the two: hold } return [{ sell: 'ETH', buy: 'USDG', amountUsd: ethUsd, reason: s.exit }] } /** The wallet's holding of `token` in dollars (0 when it holds none, or the token has no price). */ const usdOf = (book: Book, token: string) => book.balances[token]?.usd ?? 0 const pct = (x: number) => `${(x * 100).toFixed(1)}%`