An automated cryptocurrency trading bot that executes trades based on moving average crossovers on Hyperliquid perpetual futures.
A single-user application that automates a simple but effective trading strategy: buying when price is above the moving average (bullish trend) and selling when price is below (bearish trend). Connect your Hyperliquid wallet and configure your currencies, leverage, and position sizes.
The bot runs automatically at UTC midnight each day, checking all active currencies and executing trades based on the moving average signals.
- Automated Trading: Executes trades at UTC midnight based on moving average signals
- Multi-Currency Support: Track and trade multiple cryptocurrencies
- Configurable Strategy: Set custom moving average periods (e.g., 200 DMA) per currency
- Flexible Leverage: Configure different leverage for long and short positions
- Margin Allocation: Set maximum margin per currency to control position sizing
- Cross Margin: All trades use cross margin for capital efficiency
- ATR Trailing Stops: Optional ATR-based trailing stop loss with partial position closing
- Partial Closes: Incrementally close positions as they move in your favor
- Trade History: Full record of all trades with entry/exit prices and P/L
- Performance Tracking: Daily position snapshots with cumulative P/L tracking
- Wallet Tracking: Monitor wallet balance over time with historical charts
- Real-time Dashboard: Dark-themed UI for monitoring your bot
- Manual Override: Run the bot manually at any time from the dashboard
- Discord Notifications: Receive trading reports via Discord webhooks after each bot run
The bot follows a simple moving average strategy:
- At UTC Midnight: The bot checks each active currency
- Price vs MA: Compares current price against the configured moving average
- Signal Generation:
- Price > MA → Open LONG position (or do nothing if already long)
- Price < MA → Open SHORT position (or do nothing if already short)
- Position Management: If the signal changes, the bot closes the existing position and opens a new one in the opposite direction
- Snapshots: Records wallet balance and position snapshots for performance tracking
The bot supports optional ATR (Average True Range) trailing stops for risk management. When enabled, the bot will:
- Track Favorable Movement: As the position moves in your favor, the bot records the most extreme price reached
- Calculate Dynamic Stop: The stop price trails behind the extreme price by a distance of
ATR × multiplier - Partial Position Closing: When the stop is hit, the bot closes a percentage of your position (not the entire position)
- Reset and Continue: After a partial close, the stop resets and begins trailing again with the remaining position
- ATR Period: Number of periods to calculate ATR (e.g., 14)
- ATR Multiplier: Distance multiplier for the stop (e.g., 2.0 means stop is 2×ATR away)
- Close Percent: Percentage of position to close when stop is hit (e.g., 25% for quarter position)
- You're in a LONG position on BTC at $40,000
- BTC rises to $45,000 (extreme price)
- ATR is $500, multiplier is 2.0
- Stop price is $45,000 - ($500 × 2.0) = $44,000
- Price drops to $44,000 → 25% of position closes
- Remaining 75% continues, stop resets to trail new extreme price
This allows you to lock in partial profits while staying in the trend.
- Framework: Next.js 16 with App Router
- Database: PostgreSQL with Prisma ORM
- Styling: Tailwind CSS + shadcn/ui
- Charts: Recharts
- Trading: @nktkas/hyperliquid SDK
- Scheduling: node-cron
- Runtime: Bun
- Node.js 18+ or Bun
- PostgreSQL database
- Hyperliquid account with API access
- Clone the repository
git clone https://github.com/wezzcoetzee/moving-average-bot.git
cd moving-average-botUse this path when you want the app, database, and optional cron runner isolated in containers while still hot-reloading local source changes.
- Create a local environment file
cp .env.example .envSet a stable encryption key before creating bots. Keep this value unchanged for the life of your local database, because stored bot keys are encrypted with it.
openssl rand -hex 32Paste the generated value into ENCRYPTION_KEY in .env. You can leave DATABASE_URL as-is for host-based development; Docker Compose overrides it inside containers to use the db service.
Compose uses .env for variable substitution when the file is present. Local Compose runs force Hyperliquid testnet mode on by default.
- Start the local stack
docker compose up appThe app service waits for Postgres, installs dependencies into a Docker volume, runs prisma generate, pushes the Prisma schema, and starts Next.js on http://localhost:3002.
- Develop locally
- Edit files on your host machine; the app container has the repo mounted at
/appand Next.js hot reloads changes. - Follow logs with
docker compose logs -f app. - Run commands in the app container with
docker compose exec app <command>, for example:
docker compose exec app bun run lint
docker compose exec app bun run test
docker compose exec app bunx prisma studio --hostname 0.0.0.0- Attach a debugger when needed
Start the app with Node inspector enabled:
NODE_OPTIONS="--inspect=0.0.0.0:9229" docker compose up appThen attach your debugger to localhost:9229.
- Run the cron worker locally
The cron service is behind a Compose profile so it does not start during normal UI development.
docker compose --profile cron up app cronThe cron worker calls the app at http://app:3002/api/cron using the same CRON_SECRET value as the app container.
- Connect to or reset the local database
From the host, Postgres is available at:
postgresql://moving_average_bot:moving_average_bot@localhost:5432/moving_average_bot
Stop containers without deleting data:
docker compose downReset all local Docker database data:
docker compose down -v- Install dependencies
bun install- Set up environment variables
cp .env.example .envEdit .env with your configuration. See .env.example for all available options.
Required Variables:
# Database connection
DATABASE_URL="postgresql://user:password@localhost:5432/moving_average_bot"
# Encryption key for private keys at rest (generate with: openssl rand -hex 32)
ENCRYPTION_KEY=""
# Hyperliquid private key (hex, starts with 0x)
HYPERLIQUID_PRIVATE_KEY="0x..."
# Cron endpoint authentication
CRON_SECRET="your_secret_here"Optional Variables:
# Logger
LOG_LEVEL="INFO" # DEBUG | INFO | WARN | ERROR | SILENT
ENABLE_LOGS="true" # Master switch for logging
# Hyperliquid
HYPERLIQUID_TESTNET="false" # Server-side testnet flag
NEXT_PUBLIC_HYPERLIQUID_TESTNET="false" # Client-side testnet flag
# Cron
API_URL="http://localhost:3002" # Base URL for cron script
# CRON_EXPRESSION="0 0 * * *" # Override schedule (default: midnight UTC)- Set up the database
bunx prisma generate
bunx prisma db push- Run the development server
bun run devOpen http://localhost:3002 to view the dashboard.
- Set
HYPERLIQUID_PRIVATE_KEYin your.envfile - Open the dashboard and add your currencies
- Enable the bot in Settings when ready
- Click "Add Currency"
- Configure:
- Symbol: The trading pair (e.g., "BTC", "ETH")
- Name: Display name (e.g., "Bitcoin")
- Moving Average: Period in days (e.g., 200 for 200 DMA)
- Long Leverage: Leverage for long positions
- Short Leverage: Leverage for short positions
- Margin Allocation: Max margin in USD for this currency
- Enable/Disable: Toggle the bot on/off from the header
- Max Leverage: Maximum leverage applied on the exchange (default: 5)
- Margin Allocation: Controls position sizing per currency
Receive trading reports via Discord webhooks after each bot run, with P/L for each position and overall wallet status.
-
Create a Discord Webhook
- Open your Discord server and go to the channel where you want notifications
- Click the gear icon (Edit Channel) → Integrations → Webhooks
- Click "New Webhook", give it a name, and click "Copy Webhook URL"
-
Configure in Settings
- Go to Settings in the dashboard
- Paste the webhook URL in the Discord Notifications section
- Click "Save Settings"
- Click "Test" to verify it works
Complete reference for all environment variables:
| Variable | Description | Example |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://user:pass@localhost:5432/dbname |
ENCRYPTION_KEY |
64-char hex key (32 bytes) used to encrypt wallet private keys at rest. Generate with openssl rand -hex 32. |
a1b2c3... (64 hex chars) |
HYPERLIQUID_PRIVATE_KEY |
Private key for Hyperliquid wallet (hex, 0x-prefixed). Only needed for the seed migration. | 0xabc123... |
CRON_SECRET |
Secret token for authenticating /api/cron endpoint |
your-secret-token-here |
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO |
Minimum log level: DEBUG | INFO | WARN | ERROR | SILENT |
ENABLE_LOGS |
true |
Master switch to enable/disable all logging |
Examples:
LOG_LEVEL="DEBUG" # Development: Show all debug messages
LOG_LEVEL="WARN" # Production: Only warnings and errors
ENABLE_LOGS="false" # Completely silent| Variable | Default | Description |
|---|---|---|
HYPERLIQUID_TESTNET |
false |
Server-side testnet flag (API routes) |
NEXT_PUBLIC_HYPERLIQUID_TESTNET |
false |
Client-side testnet flag (browser/hooks) |
| Variable | Default | Description |
|---|---|---|
API_URL |
http://localhost:3002 |
Base URL for the cron script to call |
CRON_EXPRESSION |
0 0 * * * |
Cron schedule (default: midnight UTC daily) |
CRON_ALERT_WEBHOOK_URL |
- | Discord webhook URL for cron failure alerts (optional) |
The repo ships a Dockerfile (app), a Dockerfile.cron (scheduler), and a docker-compose.yml that wires both up alongside Postgres.
Build and run the full stack:
# ENCRYPTION_KEY is required - the stack refuses to start without it
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
docker compose up -d --build # app + database
docker compose --profile cron up -d # add the scheduled trading workerThe client-side testnet flag is baked in at image build time, so pass it as a build arg if you are building the image yourself:
docker build --build-arg NEXT_PUBLIC_HYPERLIQUID_TESTNET=true -t moving-average-bot .| Container | Description | Port |
|---|---|---|
moving-average-bot-app |
Next.js web dashboard | 3002 |
moving-average-bot-db |
PostgreSQL database | 5432 |
moving-average-bot-cron |
Runs the trading bot on a schedule (cron profile) |
- |
.github/workflows/pr-checks.yml runs lint and tests on every pull request against main and posts the results as a PR comment. There is no deployment workflow in this repo — deploy however suits your infrastructure (the Docker images above are self-contained).
- Build the application:
bun run build- Start the production server:
bun run start- Run the cron script in a separate process:
bun run scripts/cron.tsOr use PM2 for process management:
pm2 start "bun run scripts/cron.ts" --name "trading-bot-cron"The app is multi-bot: most resources are scoped under a bot ID (/api/bots/[botId]/...).
| Endpoint | Method | Description |
|---|---|---|
/api/bots |
GET | List bots |
/api/bots |
POST | Create a bot |
/api/bots/[botId] |
GET | Get a bot |
/api/bots/[botId] |
PATCH | Update bot settings |
/api/bots/[botId] |
DELETE | Delete a bot |
/api/bots/[botId]/run |
POST | Run a single bot manually |
/api/bots/run-all |
POST | Run every enabled bot |
/api/bots/[botId]/discord-test |
POST | Send a test Discord notification |
| Endpoint | Method | Description |
|---|---|---|
/api/bots/[botId]/currencies |
GET | List the bot's currencies |
/api/bots/[botId]/currencies |
POST | Add a currency |
/api/bots/[botId]/currencies/[id] |
GET | Get a currency |
/api/bots/[botId]/currencies/[id] |
PATCH | Update a currency |
/api/bots/[botId]/currencies/[id] |
DELETE | Delete a currency |
| Endpoint | Method | Description |
|---|---|---|
/api/bots/[botId]/trades |
GET | List trades |
/api/bots/[botId]/trades/open |
GET | Get open trades |
/api/bots/[botId]/trades/[id] |
GET | Get a specific trade |
/api/bots/[botId]/trades/stats |
GET | Get trading statistics |
/api/bots/[botId]/positions/close |
POST | Close a position manually |
/api/bots/[botId]/positions/history |
GET | Get position snapshot history |
/api/bots/[botId]/positions/summary |
GET | Get performance summary |
/api/bots/[botId]/pnl |
GET | Get profit and loss |
| Endpoint | Method | Description |
|---|---|---|
/api/bots/[botId]/wallet |
GET | Get wallet balance and open positions |
/api/bots/[botId]/wallet/history |
GET | Get wallet balance history |
/api/price/[symbol] |
GET | Get price and moving average for a symbol |
| Endpoint | Method | Description |
|---|---|---|
/api/health |
GET | Health check |
/api/metrics |
GET | Prometheus metrics |
/api/openapi |
GET | OpenAPI specification |
/api/settings |
GET, PATCH | Global settings |
/api/settings/env |
GET | Report which env vars are configured |
/api/cron |
GET, POST | Cron trigger (protected by CRON_SECRET) |
| Field | Type | Description |
|---|---|---|
| id | String | Unique identifier |
| symbol | String | Trading symbol (e.g., "BTC") |
| name | String | Display name |
| movingAverage | Int | MA period in days |
| longLeverage | Float | Leverage for longs |
| shortLeverage | Float | Leverage for shorts |
| marginAllocation | Float | Max margin in USD |
| isActive | Boolean | Whether to trade this currency |
| atrTrailingEnabled | Boolean | Enable ATR trailing stop |
| atrPeriod | Int | ATR calculation period |
| atrMultiplier | Float | ATR multiplier for stop distance |
| atrClosePercent | Float | % of position to close at stop |
| Field | Type | Description |
|---|---|---|
| id | String | Unique identifier |
| currencyId | String | Reference to currency |
| type | Enum | LONG or SHORT |
| status | Enum | OPEN, CLOSED, or CANCELLED |
| entryPrice | Float | Entry price |
| exitPrice | Float | Exit price (when fully closed) |
| movingAverage | Float | MA value at trade time |
| leverage | Float | Leverage used |
| profitPercent | Float | Profit/loss as % |
| profitUsd | Float | Profit/loss in USD |
| quantity | Float | Initial position size |
| remainingQuantity | Float | Remaining after partial closes |
| extremePrice | Float | Most favorable price reached |
| atrStopPrice | Float | Current ATR stop price |
| atrStopOrderId | String | Hyperliquid stop order ID |
| Field | Type | Description |
|---|---|---|
| id | String | Unique identifier |
| tradeId | String | Reference to parent trade |
| quantity | Float | Amount closed |
| exitPrice | Float | Price at close |
| profitPercent | Float | Profit/loss as % for this close |
| profitUsd | Float | Profit/loss in USD for this close |
| isAtrStop | Boolean | Whether closed by ATR stop |
| closedAt | DateTime | When position was closed |
| Field | Type | Description |
|---|---|---|
| id | String | Unique identifier |
| name | String | Bot name |
| encryptedKey | String | Wallet private key, encrypted at rest (AES-256-GCM) |
| isEnabled | Boolean | Whether the bot is active |
| maxLeverage | Float | Max leverage on exchange (default: 5) |
| lastRunAt | DateTime | Last bot execution time |
| discordWebhookUrl | String | Discord webhook URL for notifications |
| discordNotifyOnRun | Boolean | Send Discord notifications after bot runs |
| Field | Type | Description |
|---|---|---|
| id | String | Unique identifier |
| currencyId | String | Reference to currency |
| positionType | Enum | LONG, SHORT, or null |
| currentPrice | Float | Price at snapshot time |
| entryPrice | Float | Entry price of current position |
| movingAverage | Float | MA value at snapshot time |
| unrealizedPnlUsd | Float | Current unrealized P/L in USD |
| unrealizedPnlPct | Float | Current unrealized P/L as % |
| cumulativePnlUsd | Float | Total realized + unrealized P/L in USD |
| cumulativePnlPct | Float | Total realized + unrealized P/L as % |
| walletBalance | Float | Wallet balance at snapshot |
| positionSize | Float | Position size in coins |
| positionValue | Float | Position value in USD |
| Field | Type | Description |
|---|---|---|
| id | String | Unique identifier |
| balance | Float | Wallet balance at snapshot time |
| timestamp | DateTime | When snapshot was taken |
- Never commit your
.envfile — it is gitignored by default - Wallet private keys are stored in the database encrypted at rest with AES-256-GCM, using the 32-byte key from
ENCRYPTION_KEY. They are decrypted only in memory at runtime. LosingENCRYPTION_KEYmeans losing access to the stored keys, and leaking it is equivalent to leaking the wallets themselves — treat it like a password - Generate
ENCRYPTION_KEYwithopenssl rand -hex 32. Never reuse an example value - Use a strong
CRON_SECRETto protect the cron endpoint - The dashboard API routes are otherwise unauthenticated — do not expose the app directly to the public internet
- Consider using a separate wallet for the bot with limited funds
- Test on testnet first by setting
HYPERLIQUID_TESTNET=true
USE AT YOUR OWN RISK
This software is for educational purposes only. Cryptocurrency trading involves significant risk of loss. Past performance does not guarantee future results.
- This bot does not guarantee profits
- Leverage amplifies both gains and losses
- Market conditions can change rapidly
- Technical issues can occur
Always:
- Start with small amounts
- Test on testnet first
- Never invest more than you can afford to lose
- Monitor your positions regularly
MIT © Wesley Coetzee