"""Strategy cookbook: take-profit and stop-loss on a position. A drop-in strategy.py 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.py 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.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. 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. """ 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 TAKE_PROFIT = 0.10 # sell when ETH is 10% above the entry price STOP_LOSS = 0.05 # sell when ETH is 5% under the entry price ENTRY_PRICE = 0 # the entry price in dollars per ETH; 0: the ETH price on the first tick that sees the position MIN_POSITION_USD = 2 # less ETH than this (dollars) is no position: what is left after a sale def decide(market, book, state): s = state.setdefault("take_profit_stop_loss", {}) # 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 eth_usd = usd_of(book, "ETH") if eth_usd < MIN_POSITION_USD: s.clear() # no position, or it has been sold: the next one starts afresh return [] if "entry" not in s or (ENTRY_PRICE and s["entry"] != ENTRY_PRICE): s.clear() s["entry"] = ENTRY_PRICE or price # a new position (or ENTRY_PRICE changed): its entry price entry = s["entry"] if "exit" not in s: if price >= entry * (1 + TAKE_PROFIT): s["exit"] = f"take-profit: ETH ${price:,.2f} is {price / entry - 1:.1%} above the entry ${entry:,.2f}" elif price <= entry * (1 - STOP_LOSS): s["exit"] = f"stop-loss: ETH ${price:,.2f} is {1 - price / entry:.1%} under the entry ${entry:,.2f}" else: return [] # between the two: hold return [Order(sell="ETH", buy="USDG", amount_usd=eth_usd, reason=s["exit"])] def usd_of(book, token): """The wallet's holding of `token` in dollars (0 when it holds none, or the token has no price).""" return (book["balances"].get(token) or {}).get("usd") or 0