DEV Community

Cover image for Introducing Revenant: A Dollar-Cost Averaging Spot Trading Bot for Hyperliquid DEX.
cyril
cyril

Posted on

Introducing Revenant: A Dollar-Cost Averaging Spot Trading Bot for Hyperliquid DEX.

Introduction

Revenant is a lightweight, open-source DCA (Dollar-Cost-Averaging) spot trading bot for Hyperliquid decentralized exchange, written entirely in Python.

It automates recurring spot market buys at configurable intervals, with built-in position sizing, take-profit logic, and real-time status monitoring using the official Hyperliquid Python SDK.

The bot was designed from the ground up for simplicity, reliability, and zero-maintenance operation.

This documentation walks through the full development and deployment journey, from initial setup and core trading logic to packaging it as a clean Flask app and launching it as a managed web service on DigitalOcean App Platform in minutes.

By following this documentation, anyone can run the bot as-is, customize it for their own strategy, or learn how to build and deploy similar trading tools.


Initial Skeletal Setup

Before any trading logic, we start with the smallest possible Flask application. This confirms that the project structure, build process, and DigitalOcean App Platform configuration are working correctly.

Project Structure (Root Folder)

revenant/
├── app.py
├── requirements.txt
Enter fullscreen mode Exit fullscreen mode

Files
app.py

import os
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello World!"

# Optional: only used when running locally with `python app.py`
if __name__ == "__main__":
    port = int(os.getenv("PORT", 8080))
    app.run(host="0.0.0.0", port=port)
Enter fullscreen mode Exit fullscreen mode

requirements.txt

Flask
gunicorn
Enter fullscreen mode Exit fullscreen mode

Next, deploy the app on DigitalOcean App Platform.

First, create a new GitHub repository (eg. revenant) and push the two files above to the main branch.

Head over to the DigitalOcean Control Panel, navigate to App Platform, then click on Create app.

Connect your GitHub account, select the relevant repository, and the main branch. App Platform will automatically detect the Python app.

In the Web Service component settings, set the run command as gunicorn --worker-tmp-dir /dev/shm app:app, and leave the build command empty.

Click Create App

Once the build finishes, your app will be live at a URL similar to https://revenant-xxxxx.ondigitalocean.app. You should see a plain white page with the text Hello World! exactly as shown below:

This confirms the deployment pipeline is working. From here, we can safely start building the actual DCA trading logic for Revenant without worrying about infrastructure issues.


Core Hyperliquid Utilities

Now, let's add the trading helper functions that interact with the Hyperliquid DEX. These utilities handle authentication, balance checks, price fetching, and executing market buy/sell orders for the relevant spot pair.

All sensitive data (especially the private key) is loaded from environment variables. Keys are never to be committed to Git.

Create a new Python file in the root directory (utility.py):

revenant/
├── app.py
├── requirements.txt
├── utility.py          ← new file
Enter fullscreen mode Exit fullscreen mode

In the new utility.py file, add the following code:

import os
import eth_account

from hyperliquid.info import Info
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants

# Load private key from environment variable (required for DigitalOcean)
HL_PRIVATE_KEY = os.getenv("HL_PRIVATE_KEY")
if not HL_PRIVATE_KEY:
    raise ValueError("HL_PRIVATE_KEY environment variable is not set!")

# Authentication
wallet = eth_account.Account.from_key(HL_PRIVATE_KEY)
address = wallet.address

# Initialize exchange for placing orders
exchange = Exchange(
    wallet,
    base_url=constants.MAINNET_API_URL,
    account_address=address
)

# Initialize info client for reading data (REST only)
info = Info(constants.MAINNET_API_URL, skip_ws=True)

# Get USDC and HYPE balances (available balance = total - hold)
def get_balances(BASE_ASSET, QUOTE_ASSET):
    state = info.spot_user_state(wallet.address)
    balances = state.get("balances", [])

    quote_balance = 0.0
    base_balance = 0.0

    for balance in balances:
        if balance["coin"] == QUOTE_ASSET:
            quote_balance = float(balance["total"]) - float(balance["hold"])
        if balance["coin"] == BASE_ASSET:
            base_balance = float(balance["total"]) - float(balance["hold"])

    return quote_balance, base_balance

# Get current HYPE price from mid price
def get_price(BASE_ASSET):
    all_mids = info.all_mids()
    price = all_mids.get(BASE_ASSET)
    return float(price) if price else None

# Execute market sell of HYPE
def execute_sell(BASE_ASSET, QUOTE_ASSET, base_balance: float):
    sell = exchange.market_open(
        name=f"{BASE_ASSET}/{QUOTE_ASSET}",
        is_buy=False,
        sz=round(base_balance, 2),
        slippage=0.002
    )
    return sell

# Execute market buy of HYPE with USDC amount
def execute_buy(BASE_ASSET, QUOTE_ASSET,buy_size_usdc: float):
    if get_price(BASE_ASSET) is None:
        raise ValueError(f"Could not fetch {BASE_ASSET} price")

    size = buy_size_usdc / get_price(BASE_ASSET)
    buy = exchange.market_open(
        name=f"{BASE_ASSET}/{QUOTE_ASSET}",
        is_buy=True,
        sz=round(size, 2),
        slippage=0.002
    )
    return buy

# Get the most recent fill (useful for logging/tracking)
def get_most_recent_fill():
    fills = info.user_fills(address)
    if not fills:
        return None
    return fills[0]
Enter fullscreen mode Exit fullscreen mode

Then update the requirements.txt file to reflect the newly added libraries, hyperliquid-python-sdk, eth-account, and protobuf:

Flask
gunicorn
hyperliquid-python-sdk
eth-account
protobuf
Enter fullscreen mode Exit fullscreen mode

Following the introduction of a new environment variable, let's update DigitalOcean's Environment Variables.

Go to your app in the DigitalOcean control panel and click "Edit" on the Web Service component. Scroll to Environmental Variables and add a new variable:

  • Key: HL_PRIVATE_KEY
  • Value: 0x...

Click Save → Deploy (or let it auto-deploy on push).


Core DCA Trading Logic

We now replace the minimal "Hello World" Flask app with a complete trading engine for Revenant.

The bot implements a simple but effective DCA strategy on the HYPE/USDC spot pair:

  • Buys $10 worth of HYPE on every run (unless USDC balance is too low).
  • Continuously averages the buy price.
  • When the current price reaches +3% the average buy price, it sells the entire HYPE position, records the PnL, and starts a new session.
  • All states are tracked in a global counter dictionary (in-memory for now).

Full app.py:

import os, time
from flask import Flask, jsonify
import utility
import datetime

# Add background scheduler imports
from apscheduler.schedulers.background import BackgroundScheduler
import atexit

app = Flask(__name__)

# Define global variables
counter = {
    "avg_buy_price": float(0.0),
    "total_sessions": 0,
    "buy_size_usdc": float(10.0),
    "buy_count": 0,
    "total_buy_trades": 0,
    "total_volume_usdc": float(0.0)
}

BASE_ASSET = "HYPE"
QUOTE_ASSET = "USDC"

@app.route('/')
def home():
    return jsonify(counter)


@app.route("/execute")
def execute():

    # Initialize status
    status = ""

    # Fetch current market data, update AUM
    quote_balance, base_balance = utility.get_balances(BASE_ASSET, QUOTE_ASSET)
    price = utility.get_price(BASE_ASSET)
    AUM = float(quote_balance) + (float(base_balance) * float(price))

    # Save balances
    counter["base_balance"] = base_balance
    counter["quote_balance"] = quote_balance
    counter["price"] = price
    counter["AUM"] = AUM

    # Start/continue trading
    # Check if sell condition is met.
    sell_condition_is_met = False

    if float(price) >= float(counter["avg_buy_price"]) * 0.03 and float(counter["avg_buy_price"]) != float(0):
        sell_condition_is_met = True

    # if sell condition is met, liquidate hype position, end session, and start a new session. else keep buying.
    if sell_condition_is_met:
        # liquidate hype position (sell all of hype balance)
        sell = utility.execute_sell(BASE_ASSET, QUOTE_ASSET, base_balance)

        # get fill price and calculate pnl in usdc
        time.sleep(5)

        most_recent_fill = utility.get_most_recent_fill()

        avg_fill_price = most_recent_fill["px"]
        pnl_usdc = (float(avg_fill_price) - float(counter["avg_buy_price"])) * float(counter["base_balance"]) # (avg fill price - avg buy price) * total hype bought

        # update counter
        counter["pnl_usdc"] = pnl_usdc
        counter["total_sessions"] += 1
        counter["total_volume_usdc"] += (float(base_balance) * float(avg_fill_price))
        counter["last_trade"] = datetime.datetime.now(datetime.UTC)

        status = "Sell condition met, HYPE holdings sold."
    else:
        # keep buying with a constant size of $10
        buy_size_usdc = counter["buy_size_usdc"]

        # Execute buy if buy size is less than or equal to the available balance
        if buy_size_usdc <= quote_balance:

            # execute buy
            buy = utility.execute_buy(BASE_ASSET, QUOTE_ASSET, float(buy_size_usdc))

            # wait 5 seconds, get most recent fill details
            time.sleep(5)

            most_recent_fill = utility.get_most_recent_fill()
            avg_fill_price = most_recent_fill["px"]

            if float(counter["avg_buy_price"]) == float(0):
                counter["avg_buy_price"] = float(avg_fill_price)
            else:
                counter["avg_buy_price"] = (float(counter["avg_buy_price"]) + float(avg_fill_price))/2


            # Update counter
            counter["buy_count"] += 1
            counter["total_buy_trades"] += 1
            counter["total_volume_usdc"] += float(buy_size_usdc)
            counter["last_trade"] = datetime.datetime.now(datetime.UTC)

            status =  "Sell condition not met, buy executed."

        else:
            status  = "Sell condition not met, balance low." # buy nothing when balance low, wait for next 15 minutes

    return jsonify(status) 

# ── Background Scheduler (runs automatically every hour) ──
scheduler = BackgroundScheduler()

def scheduled_execute():
    """Wrapper that runs the trading cycle from the background job."""
    with app.app_context():
        execute()

scheduler.add_job(
    scheduled_execute,
    trigger='interval',
    hours=1,
    id='revenant_dca_cycle',
    replace_existing=True
)
scheduler.start()

# Gracefully shut down scheduler when the app exits
atexit.register(lambda: scheduler.shutdown(wait=False))

# Optional: only used when running locally with `python app.py`
if __name__ == "__main__":
    port = int(os.getenv("PORT", 8080))
    app.run(host="0.0.0.0", port=port)
Enter fullscreen mode Exit fullscreen mode

How the Strategy Works (Summary)

Action Condition What Happens
Buy Price < 3% avg_buy_price Buy $10 USDC of HYPE
Sell (Take profit) Price ≥ 3% avg_buy_price Sell all HYPE, record PnL, reset session
No-op Insufficient USDC balance Wait for next cycle

Next, let's add a background scheduler (APScheduler) so the bot automatically runs the /execute endpoint every 1 hour.


Automatic Trading Every Hour

Add a background scheduler using APScheduler so Revenant automatically runs the full DCA trading cycle (/execute logic) every 1 hour. No manual calls or external cron jobs required.

Add APScheduler to the requirements.txt:

Flask
gunicorn
hyperliquid
eth-account
APScheduler
Enter fullscreen mode Exit fullscreen mode

Next, add background scheduler imports:

from apscheduler.schedulers.background import BackgroundScheduler
import atexit
Enter fullscreen mode Exit fullscreen mode

Then add the scheduler code beneath the trade logic execution:

# ── Background Scheduler (runs automatically every hour) ──
scheduler = BackgroundScheduler()

def scheduled_execute():
    """Wrapper that runs the trading cycle from the background job."""
    with app.app_context():
        execute()

scheduler.add_job(
    scheduled_execute,
    trigger='interval',
    hours=1,
    id='revenant_dca_cycle',
    replace_existing=True
)
scheduler.start()

# Gracefully shut down scheduler when the app exits
atexit.register(lambda: scheduler.shutdown(wait=False))
Enter fullscreen mode Exit fullscreen mode

Update Run command on DigitalOcean

To prevent the scheduler from starting multiple times (Gunicorn forks workers by default):

  • Go to your app in DigitalOcean Control Panel → Components → your Web Service → Edit.
  • Change the Run command to: gunicorn --worker-tmp-dir /dev/shm --preload --workers=1 app:app
  • Save and Redeploy.

The --preload flag ensures the scheduler is created only once before workers are forked.

Current Limitations

  • State is stored only in memory → resets when the app restarts.
  • No error handling or retry logic around API calls yet.

Conclusion

Revenant is now a fully functional, automated DCA spot-trading bot running live on the DigitalOcean App Platform.

It connects securely to Hyperliquid, executes $10 USDC buys every hour, tracks average entry price, and automatically takes profit by selling the entire position when the price doubles.

The entire project uses a minimal, clean Flask + APScheduler setup that deploys in minutes and requires zero server management.

This is a solid foundation to extend: add persistent storage, configurable parameters via environment variables, a web dashboard, error alerts, or even multi-pair support.

Top comments (0)