aster-bot-trading

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Aster Trading Bot

Aster交易机器人

Skill by ara.so — Daily 2026 Skills collection.
Aster Bot is a TypeScript/Node.js automated trading system for ASTERUSDT perpetual futures on AsterDEX. It features dual strategy engines (Watermellon and Peach Hybrid), configurable risk controls, real-time WebSocket market data, and production-grade logging with CSV/JSON trade records.

ara.so提供的Skill — 2026每日Skill合集。
Aster Bot是一款基于TypeScript/Node.js的自动化交易系统,用于AsterDEX上的ASTERUSDT永续合约交易。它具备双策略引擎(Watermellon和Peach Hybrid)、可配置的风险控制、实时WebSocket市场数据,以及支持CSV/JSON交易记录的生产级日志功能。

Installation

安装

bash
git clone https://github.com/SignalBot-Labs/aster-bot.git
cd aster-bot
npm install
cp env.example .env.local
Edit
.env.local
with your credentials (see Configuration below), then:
bash
undefined
bash
git clone https://github.com/SignalBot-Labs/aster-bot.git
cd aster-bot
npm install
cp env.example .env.local
编辑
.env.local
文件,填入你的凭证(详见下方配置部分),然后执行:
bash
undefined

Dry-run (no real orders)

模拟运行(无真实订单)

npm run bot
npm run bot

Live trading (real orders, real risk)

实盘交易(真实订单,存在真实风险)

MODE=live npm run bot

---
MODE=live npm run bot

---

Configuration

配置

All configuration is via environment variables in
.env.local
.
所有配置均通过
.env.local
文件中的环境变量进行设置。

Required

必填配置

env
ASTER_RPC_URL=https://fapi.asterdex.com
ASTER_WS_URL=wss://fstream.asterdex.com/ws
ASTER_API_KEY=$ASTER_API_KEY
ASTER_API_SECRET=$ASTER_API_SECRET
TRADING_WALLET_PRIVATE_KEY=$TRADING_WALLET_PRIVATE_KEY   # 64-char hex EVM key
PAIR_SYMBOL=ASTERUSDT-PERP
MODE=dry-run   # or: live
env
ASTER_RPC_URL=https://fapi.asterdex.com
ASTER_WS_URL=wss://fstream.asterdex.com/ws
ASTER_API_KEY=$ASTER_API_KEY
ASTER_API_SECRET=$ASTER_API_SECRET
TRADING_WALLET_PRIVATE_KEY=$TRADING_WALLET_PRIVATE_KEY   # 64位十六进制EVM密钥
PAIR_SYMBOL=ASTERUSDT-PERP
MODE=dry-run   # 可选值:live

Risk Management

风险管理

env
MAX_POSITION_USDT=10000
MAX_LEVERAGE=5           # Must be one of: 5, 10, 15, 50
MAX_FLIPS_PER_HOUR=12
STOP_LOSS_PCT=0
TAKE_PROFIT_PCT=0
USE_STOP_LOSS=false
EMERGENCY_STOP_LOSS_PCT=2.0
MAX_POSITIONS=1
REQUIRE_TRENDING_MARKET=true
ADX_THRESHOLD=25
env
MAX_POSITION_USDT=10000
MAX_LEVERAGE=5           # 可选值:5, 10, 15, 50
MAX_FLIPS_PER_HOUR=12
STOP_LOSS_PCT=0
TAKE_PROFIT_PCT=0
USE_STOP_LOSS=false
EMERGENCY_STOP_LOSS_PCT=2.0
MAX_POSITIONS=1
REQUIRE_TRENDING_MARKET=true
ADX_THRESHOLD=25

Strategy Selection

策略选择

env
STRATEGY_TYPE=peach-hybrid   # or: watermellon
env
STRATEGY_TYPE=peach-hybrid   # 可选值:watermellon

Timeframe

时间框架

env
VIRTUAL_TIMEFRAME_MS=30000   # Bar size in ms (e.g. 30000 = 30s bars)
env
VIRTUAL_TIMEFRAME_MS=30000   # K线周期(毫秒),例如30000=30秒K线

Startup Price Guard

启动价格防护

The bot calls
web3.prc
's
prices()
at startup and checks the
responsive
field against
limitPrice = 0.871
in
src/lib/spotPrice.ts
. If below, the bot exits.
env
SKIP_MIN_SPOT_CHECK=true   # Skip price gate for local testing only

机器人启动时会调用
web3.prc
prices()
方法,并在
src/lib/spotPrice.ts
中检查
responsive
字段是否低于
limitPrice = 0.871
。如果低于该值,机器人将退出运行。
env
SKIP_MIN_SPOT_CHECK=true   # 仅在本地测试时跳过价格检查

Strategy Configuration

策略配置

Watermellon (EMA + RSI trend following)

Watermellon(EMA + RSI趋势跟踪策略)

env
STRATEGY_TYPE=watermellon
EMA_FAST=8
EMA_MID=21
EMA_SLOW=48
RSI_LENGTH=14
RSI_MIN_LONG=42
RSI_MAX_SHORT=58
Logic:
  • Long: bullish EMA stack (fast > mid > slow) + RSI ≥
    RSI_MIN_LONG
    + ADX ≥
    ADX_THRESHOLD
  • Short: bearish EMA stack (fast < mid < slow) + RSI ≤
    RSI_MAX_SHORT
    + ADX ≥
    ADX_THRESHOLD
env
STRATEGY_TYPE=watermellon
EMA_FAST=8
EMA_MID=21
EMA_SLOW=48
RSI_LENGTH=14
RSI_MIN_LONG=42
RSI_MAX_SHORT=58
逻辑:
  • 做多: 看涨EMA排列(快线 > 中线 > 慢线) + RSI ≥
    RSI_MIN_LONG
    + ADX ≥
    ADX_THRESHOLD
  • 做空: 看跌EMA排列(快线 < 中线 < 慢线) + RSI ≤
    RSI_MAX_SHORT
    + ADX ≥
    ADX_THRESHOLD

Peach Hybrid (Dual V1 + V2 system)

Peach Hybrid(V1 + V2双系统策略)

env
STRATEGY_TYPE=peach-hybrid
env
STRATEGY_TYPE=peach-hybrid

V1 — trend/bias layer

V1 — 趋势/方向判断层

PEACH_V1_EMA_FAST=8 PEACH_V1_EMA_MID=21 PEACH_V1_EMA_SLOW=48 PEACH_V1_EMA_MICRO_FAST=5 PEACH_V1_EMA_MICRO_SLOW=13 PEACH_V1_RSI_LENGTH=14 PEACH_V1_RSI_MIN_LONG=42.0 PEACH_V1_RSI_MAX_SHORT=58.0 PEACH_V1_MIN_BARS_BETWEEN=1 PEACH_V1_MIN_MOVE_PCT=0.10
PEACH_V1_EMA_FAST=8 PEACH_V1_EMA_MID=21 PEACH_V1_EMA_SLOW=48 PEACH_V1_EMA_MICRO_FAST=5 PEACH_V1_EMA_MICRO_SLOW=13 PEACH_V1_RSI_LENGTH=14 PEACH_V1_RSI_MIN_LONG=42.0 PEACH_V1_RSI_MAX_SHORT=58.0 PEACH_V1_MIN_BARS_BETWEEN=1 PEACH_V1_MIN_MOVE_PCT=0.10

V2 — momentum surge layer

V2 — 动量爆发层

PEACH_V2_EMA_FAST=3 PEACH_V2_EMA_MID=8 PEACH_V2_EMA_SLOW=13 PEACH_V2_RSI_MOMENTUM_THRESHOLD=3.0 PEACH_V2_VOLUME_LOOKBACK=4 PEACH_V2_VOLUME_MULTIPLIER=1.5 PEACH_V2_EXIT_VOLUME_MULTIPLIER=1.2

---
PEACH_V2_EMA_FAST=3 PEACH_V2_EMA_MID=8 PEACH_V2_EMA_SLOW=13 PEACH_V2_RSI_MOMENTUM_THRESHOLD=3.0 PEACH_V2_VOLUME_LOOKBACK=4 PEACH_V2_VOLUME_MULTIPLIER=1.5 PEACH_V2_EXIT_VOLUME_MULTIPLIER=1.2

---

Key Commands

关键命令

bash
undefined
bash
undefined

Start the bot (dry-run by default)

启动机器人(默认模拟运行)

npm run bot
npm run bot

TypeScript compilation check

TypeScript编译检查

npx tsc --noEmit
npx tsc --noEmit

Build

构建项目

npm run build
npm run build

Run compiled output

运行编译后的代码

npm run start

---
npm run start

---

Project Structure

项目结构

aster-bot/
├── src/
│   ├── bot.ts                  # Main entry point
│   ├── lib/
│   │   ├── spotPrice.ts        # Startup price guard (limitPrice = 0.871)
│   │   ├── logger.ts           # Console + file logging
│   │   └── state.ts            # Persistent state across restarts
│   ├── strategies/
│   │   ├── watermellon.ts      # EMA+RSI trend strategy
│   │   └── peachHybrid.ts      # V1+V2 dual strategy
│   ├── execution/
│   │   └── orderManager.ts     # Order placement, reconciliation
│   └── risk/
│       └── riskManager.ts      # Position limits, stop-loss, flip control
├── data/
│   ├── trades/daily/           # CSV/JSON trade logs
│   └── img/                    # Reference chart screenshots
├── env.example                 # Template for .env.local
└── package.json

aster-bot/
├── src/
│   ├── bot.ts                  # 主入口文件
│   ├── lib/
│   │   ├── spotPrice.ts        # 启动价格防护(limitPrice = 0.871)
│   │   ├── logger.ts           # 控制台+文件日志
│   │   └── state.ts            # 重启后保持的持久化状态
│   ├── strategies/
│   │   ├── watermellon.ts      # EMA+RSI趋势策略
│   │   └── peachHybrid.ts      # V1+V2双策略
│   ├── execution/
│   │   └── orderManager.ts     # 订单下单、对账
│   └── risk/
│       └── riskManager.ts      # 仓位限制、止损、翻转控制
├── data/
│   ├── trades/daily/           # CSV/JSON交易日志
│   └── img/                    # 参考图表截图
├── env.example                 # .env.local模板文件
└── package.json

Real Code Examples

代码示例

Reading current configuration in TypeScript

在TypeScript中读取当前配置

typescript
// src/config.ts
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });

export const config = {
  rpcUrl: process.env.ASTER_RPC_URL ?? 'https://fapi.asterdex.com',
  wsUrl: process.env.ASTER_WS_URL ?? 'wss://fstream.asterdex.com/ws',
  apiKey: process.env.ASTER_API_KEY!,
  apiSecret: process.env.ASTER_API_SECRET!,
  privateKey: process.env.TRADING_WALLET_PRIVATE_KEY!,
  symbol: process.env.PAIR_SYMBOL ?? 'ASTERUSDT-PERP',
  mode: (process.env.MODE ?? 'dry-run') as 'dry-run' | 'live',
  maxPositionUsdt: Number(process.env.MAX_POSITION_USDT ?? 10000),
  maxLeverage: Number(process.env.MAX_LEVERAGE ?? 5),
  maxFlipsPerHour: Number(process.env.MAX_FLIPS_PER_HOUR ?? 12),
  emergencyStopLossPct: Number(process.env.EMERGENCY_STOP_LOSS_PCT ?? 2.0),
  adxThreshold: Number(process.env.ADX_THRESHOLD ?? 25),
  requireTrending: process.env.REQUIRE_TRENDING_MARKET === 'true',
  strategyType: (process.env.STRATEGY_TYPE ?? 'peach-hybrid') as 'watermellon' | 'peach-hybrid',
  virtualTimeframeMs: Number(process.env.VIRTUAL_TIMEFRAME_MS ?? 30000),
  skipMinSpotCheck: process.env.SKIP_MIN_SPOT_CHECK === 'true',
};

// Validate leverage
const VALID_LEVERAGES = [5, 10, 15, 50];
if (!VALID_LEVERAGES.includes(config.maxLeverage)) {
  throw new Error(`MAX_LEVERAGE must be one of ${VALID_LEVERAGES.join(', ')}, got ${config.maxLeverage}`);
}

// Validate private key
if (!config.privateKey || config.privateKey.length !== 64) {
  throw new Error('TRADING_WALLET_PRIVATE_KEY must be a 64-character hex string');
}
typescript
// src/config.ts
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });

export const config = {
  rpcUrl: process.env.ASTER_RPC_URL ?? 'https://fapi.asterdex.com',
  wsUrl: process.env.ASTER_WS_URL ?? 'wss://fstream.asterdex.com/ws',
  apiKey: process.env.ASTER_API_KEY!,
  apiSecret: process.env.ASTER_API_SECRET!,
  privateKey: process.env.TRADING_WALLET_PRIVATE_KEY!,
  symbol: process.env.PAIR_SYMBOL ?? 'ASTERUSDT-PERP',
  mode: (process.env.MODE ?? 'dry-run') as 'dry-run' | 'live',
  maxPositionUsdt: Number(process.env.MAX_POSITION_USDT ?? 10000),
  maxLeverage: Number(process.env.MAX_LEVERAGE ?? 5),
  maxFlipsPerHour: Number(process.env.MAX_FLIPS_PER_HOUR ?? 12),
  emergencyStopLossPct: Number(process.env.EMERGENCY_STOP_LOSS_PCT ?? 2.0),
  adxThreshold: Number(process.env.ADX_THRESHOLD ?? 25),
  requireTrending: process.env.REQUIRE_TRENDING_MARKET === 'true',
  strategyType: (process.env.STRATEGY_TYPE ?? 'peach-hybrid') as 'watermellon' | 'peach-hybrid',
  virtualTimeframeMs: Number(process.env.VIRTUAL_TIMEFRAME_MS ?? 30000),
  skipMinSpotCheck: process.env.SKIP_MIN_SPOT_CHECK === 'true',
};

// 验证杠杆倍数
const VALID_LEVERAGES = [5, 10, 15, 50];
if (!VALID_LEVERAGES.includes(config.maxLeverage)) {
  throw new Error(`MAX_LEVERAGE必须是${VALID_LEVERAGES.join(', ')}中的一个,当前值为${config.maxLeverage}`);
}

// 验证私钥
if (!config.privateKey || config.privateKey.length !== 64) {
  throw new Error('TRADING_WALLET_PRIVATE_KEY必须是64位十六进制字符串');
}

Implementing a custom indicator (EMA calculation)

实现自定义指标(EMA计算)

typescript
// src/indicators/ema.ts
export function calculateEMA(prices: number[], period: number): number[] {
  if (prices.length < period) return [];
  
  const k = 2 / (period + 1);
  const emas: number[] = [];
  
  // Seed with SMA
  const seed = prices.slice(0, period).reduce((a, b) => a + b, 0) / period;
  emas.push(seed);
  
  for (let i = period; i < prices.length; i++) {
    emas.push(prices[i] * k + emas[emas.length - 1] * (1 - k));
  }
  
  return emas;
}

export function calculateRSI(prices: number[], period: number = 14): number[] {
  if (prices.length < period + 1) return [];
  
  const rsis: number[] = [];
  let avgGain = 0;
  let avgLoss = 0;

  for (let i = 1; i <= period; i++) {
    const change = prices[i] - prices[i - 1];
    if (change > 0) avgGain += change;
    else avgLoss += Math.abs(change);
  }
  avgGain /= period;
  avgLoss /= period;

  for (let i = period; i < prices.length - 1; i++) {
    const change = prices[i + 1] - prices[i];
    const gain = change > 0 ? change : 0;
    const loss = change < 0 ? Math.abs(change) : 0;
    avgGain = (avgGain * (period - 1) + gain) / period;
    avgLoss = (avgLoss * (period - 1) + loss) / period;
    const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
    rsis.push(100 - 100 / (1 + rs));
  }

  return rsis;
}
typescript
// src/indicators/ema.ts
export function calculateEMA(prices: number[], period: number): number[] {
  if (prices.length < period) return [];
  
  const k = 2 / (period + 1);
  const emas: number[] = [];
  
  // 用SMA初始化
  const seed = prices.slice(0, period).reduce((a, b) => a + b, 0) / period;
  emas.push(seed);
  
  for (let i = period; i < prices.length; i++) {
    emas.push(prices[i] * k + emas[emas.length - 1] * (1 - k));
  }
  
  return emas;
}

export function calculateRSI(prices: number[], period: number = 14): number[] {
  if (prices.length < period + 1) return [];
  
  const rsis: number[] = [];
  let avgGain = 0;
  let avgLoss = 0;

  for (let i = 1; i <= period; i++) {
    const change = prices[i] - prices[i - 1];
    if (change > 0) avgGain += change;
    else avgLoss += Math.abs(change);
  }
  avgGain /= period;
  avgLoss /= period;

  for (let i = period; i < prices.length - 1; i++) {
    const change = prices[i + 1] - prices[i];
    const gain = change > 0 ? change : 0;
    const loss = change < 0 ? Math.abs(change) : 0;
    avgGain = (avgGain * (period - 1) + gain) / period;
    avgLoss = (avgLoss * (period - 1) + loss) / period;
    const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
    rsis.push(100 - 100 / (1 + rs));
  }

  return rsis;
}

Watermellon strategy signal generation

Watermellon策略信号生成

typescript
// src/strategies/watermellon.ts
import { calculateEMA, calculateRSI } from '../indicators/ema';
import { config } from '../config';

export type Signal = 'long' | 'short' | 'none';

export interface Bar {
  close: number;
  volume: number;
  timestamp: number;
}

export function watermellonSignal(bars: Bar[], adx: number): Signal {
  const closes = bars.map(b => b.close);
  
  const emaFast = calculateEMA(closes, Number(process.env.EMA_FAST ?? 8));
  const emaMid  = calculateEMA(closes, Number(process.env.EMA_MID  ?? 21));
  const emaSlow = calculateEMA(closes, Number(process.env.EMA_SLOW ?? 48));
  const rsi     = calculateRSI(closes, Number(process.env.RSI_LENGTH ?? 14));

  if (!emaFast.length || !emaMid.length || !emaSlow.length || !rsi.length) {
    return 'none';
  }

  const fast = emaFast[emaFast.length - 1];
  const mid  = emaMid[emaMid.length - 1];
  const slow = emaSlow[emaSlow.length - 1];
  const currentRsi = rsi[rsi.length - 1];

  const rsiMinLong  = Number(process.env.RSI_MIN_LONG  ?? 42);
  const rsiMaxShort = Number(process.env.RSI_MAX_SHORT ?? 58);

  const trendingOk = !config.requireTrending || adx >= config.adxThreshold;

  if (fast > mid && mid > slow && currentRsi >= rsiMinLong && trendingOk) {
    return 'long';
  }
  if (fast < mid && mid < slow && currentRsi <= rsiMaxShort && trendingOk) {
    return 'short';
  }
  return 'none';
}
typescript
// src/strategies/watermellon.ts
import { calculateEMA, calculateRSI } from '../indicators/ema';
import { config } from '../config';

export type Signal = 'long' | 'short' | 'none';

export interface Bar {
  close: number;
  volume: number;
  timestamp: number;
}

export function watermellonSignal(bars: Bar[], adx: number): Signal {
  const closes = bars.map(b => b.close);
  
  const emaFast = calculateEMA(closes, Number(process.env.EMA_FAST ?? 8));
  const emaMid  = calculateEMA(closes, Number(process.env.EMA_MID  ?? 21));
  const emaSlow = calculateEMA(closes, Number(process.env.EMA_SLOW ?? 48));
  const rsi     = calculateRSI(closes, Number(process.env.RSI_LENGTH ?? 14));

  if (!emaFast.length || !emaMid.length || !emaSlow.length || !rsi.length) {
    return 'none';
  }

  const fast = emaFast[emaFast.length - 1];
  const mid  = emaMid[emaMid.length - 1];
  const slow = emaSlow[emaSlow.length - 1];
  const currentRsi = rsi[rsi.length - 1];

  const rsiMinLong  = Number(process.env.RSI_MIN_LONG  ?? 42);
  const rsiMaxShort = Number(process.env.RSI_MAX_SHORT ?? 58);

  const trendingOk = !config.requireTrending || adx >= config.adxThreshold;

  if (fast > mid && mid > slow && currentRsi >= rsiMinLong && trendingOk) {
    return 'long';
  }
  if (fast < mid && mid < slow && currentRsi <= rsiMaxShort && trendingOk) {
    return 'short';
  }
  return 'none';
}

Peach Hybrid V2 momentum check

Peach Hybrid V2动量检查

typescript
// src/strategies/peachHybrid.ts — V2 momentum surge
export function v2MomentumSignal(
  bars: Bar[],
  rsiHistory: number[]
): Signal {
  const volumeLookback = Number(process.env.PEACH_V2_VOLUME_LOOKBACK ?? 4);
  const volMultiplier  = Number(process.env.PEACH_V2_VOLUME_MULTIPLIER ?? 1.5);
  const rsiThreshold   = Number(process.env.PEACH_V2_RSI_MOMENTUM_THRESHOLD ?? 3.0);

  if (bars.length < volumeLookback + 1 || rsiHistory.length < 2) return 'none';

  const recentBars = bars.slice(-volumeLookback - 1);
  const avgVolume = recentBars.slice(0, -1)
    .reduce((sum, b) => sum + b.volume, 0) / volumeLookback;
  const lastVolume = recentBars[recentBars.length - 1].volume;
  const volumeSurge = lastVolume > avgVolume * volMultiplier;

  const rsiChange = rsiHistory[rsiHistory.length - 1] - rsiHistory[rsiHistory.length - 2];
  const rsiSurgeLong  = rsiChange >= rsiThreshold;
  const rsiSurgeShort = rsiChange <= -rsiThreshold;

  if (volumeSurge && rsiSurgeLong)  return 'long';
  if (volumeSurge && rsiSurgeShort) return 'short';
  return 'none';
}
typescript
// src/strategies/peachHybrid.ts — V2动量爆发
export function v2MomentumSignal(
  bars: Bar[],
  rsiHistory: number[]
): Signal {
  const volumeLookback = Number(process.env.PEACH_V2_VOLUME_LOOKBACK ?? 4);
  const volMultiplier  = Number(process.env.PEACH_V2_VOLUME_MULTIPLIER ?? 1.5);
  const rsiThreshold   = Number(process.env.PEACH_V2_RSI_MOMENTUM_THRESHOLD ?? 3.0);

  if (bars.length < volumeLookback + 1 || rsiHistory.length < 2) return 'none';

  const recentBars = bars.slice(-volumeLookback - 1);
  const avgVolume = recentBars.slice(0, -1)
    .reduce((sum, b) => sum + b.volume, 0) / volumeLookback;
  const lastVolume = recentBars[recentBars.length - 1].volume;
  const volumeSurge = lastVolume > avgVolume * volMultiplier;

  const rsiChange = rsiHistory[rsiHistory.length - 1] - rsiHistory[rsiHistory.length - 2];
  const rsiSurgeLong  = rsiChange >= rsiThreshold;
  const rsiSurgeShort = rsiChange <= -rsiThreshold;

  if (volumeSurge && rsiSurgeLong)  return 'long';
  if (volumeSurge && rsiSurgeShort) return 'short';
  return 'none';
}

AsterDEX REST API order placement

AsterDEX REST API下单

typescript
// src/execution/orderManager.ts
import crypto from 'crypto';
import { config } from '../config';

interface OrderParams {
  symbol: string;
  side: 'BUY' | 'SELL';
  type: 'MARKET' | 'LIMIT';
  quantity: number;
  price?: number;
  reduceOnly?: boolean;
}

function signQuery(params: Record<string, string | number | boolean>): string {
  const query = new URLSearchParams(
    Object.entries(params).map(([k, v]) => [k, String(v)])
  ).toString();
  const sig = crypto
    .createHmac('sha256', config.apiSecret)
    .update(query)
    .digest('hex');
  return `${query}&signature=${sig}`;
}

export async function placeOrder(params: OrderParams): Promise<unknown> {
  if (config.mode === 'dry-run') {
    console.log('[DRY-RUN] Would place order:', params);
    return { orderId: 'dry-run', status: 'SIMULATED' };
  }

  const timestamp = Date.now();
  const body = signQuery({ ...params, timestamp, recvWindow: 5000 });

  const response = await fetch(`${config.rpcUrl}/fapi/v1/order`, {
    method: 'POST',
    headers: {
      'X-MBX-APIKEY': config.apiKey,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body,
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`Order failed: ${response.status} ${err}`);
  }

  return response.json();
}

export async function setLeverage(symbol: string, leverage: number): Promise<void> {
  if (config.mode === 'dry-run') return;
  
  const timestamp = Date.now();
  const body = signQuery({ symbol, leverage, timestamp });

  await fetch(`${config.rpcUrl}/fapi/v1/leverage`, {
    method: 'POST',
    headers: { 'X-MBX-APIKEY': config.apiKey, 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });
}
typescript
// src/execution/orderManager.ts
import crypto from 'crypto';
import { config } from '../config';

interface OrderParams {
  symbol: string;
  side: 'BUY' | 'SELL';
  type: 'MARKET' | 'LIMIT';
  quantity: number;
  price?: number;
  reduceOnly?: boolean;
}

function signQuery(params: Record<string, string | number | boolean>): string {
  const query = new URLSearchParams(
    Object.entries(params).map(([k, v]) => [k, String(v)])
  ).toString();
  const sig = crypto
    .createHmac('sha256', config.apiSecret)
    .update(query)
    .digest('hex');
  return `${query}&signature=${sig}`;
}

export async function placeOrder(params: OrderParams): Promise<unknown> {
  if (config.mode === 'dry-run') {
    console.log('[模拟运行] 将要下单:', params);
    return { orderId: 'dry-run', status: 'SIMULATED' };
  }

  const timestamp = Date.now();
  const body = signQuery({ ...params, timestamp, recvWindow: 5000 });

  const response = await fetch(`${config.rpcUrl}/fapi/v1/order`, {
    method: 'POST',
    headers: {
      'X-MBX-APIKEY': config.apiKey,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body,
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`下单失败: ${response.status} ${err}`);
  }

  return response.json();
}

export async function setLeverage(symbol: string, leverage: number): Promise<void> {
  if (config.mode === 'dry-run') return;
  
  const timestamp = Date.now();
  const body = signQuery({ symbol, leverage, timestamp });

  await fetch(`${config.rpcUrl}/fapi/v1/leverage`, {
    method: 'POST',
    headers: { 'X-MBX-APIKEY': config.apiKey, 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });
}

WebSocket market data subscription

WebSocket市场数据订阅

typescript
// src/ws/marketData.ts
import WebSocket from 'ws';
import { config } from '../config';

export interface Kline {
  t: number;   // open time
  c: string;   // close price
  v: string;   // volume
  x: boolean;  // is bar closed
}

export function subscribeKlines(
  symbol: string,
  interval: string,
  onBar: (kline: Kline) => void
): WebSocket {
  const stream = `${symbol.toLowerCase()}@kline_${interval}`;
  const ws = new WebSocket(`${config.wsUrl}/${stream}`);

  ws.on('message', (raw) => {
    try {
      const msg = JSON.parse(raw.toString());
      if (msg.k) onBar(msg.k as Kline);
    } catch { /* ignore parse errors */ }
  });

  ws.on('error', (err) => console.error('[WS] Error:', err.message));
  ws.on('close', () => {
    console.warn('[WS] Disconnected, reconnecting in 5s...');
    setTimeout(() => subscribeKlines(symbol, interval, onBar), 5000);
  });

  return ws;
}
typescript
// src/ws/marketData.ts
import WebSocket from 'ws';
import { config } from '../config';

export interface Kline {
  t: number;   // 开盘时间
  c: string;   // 收盘价
  v: string;   // 成交量
  x: boolean;  // K线是否闭合
}

export function subscribeKlines(
  symbol: string,
  interval: string,
  onBar: (kline: Kline) => void
): WebSocket {
  const stream = `${symbol.toLowerCase()}@kline_${interval}`;
  const ws = new WebSocket(`${config.wsUrl}/${stream}`);

  ws.on('message', (raw) => {
    try {
      const msg = JSON.parse(raw.toString());
      if (msg.k) onBar(msg.k as Kline);
    } catch { /* 忽略解析错误 */ }
  });

  ws.on('error', (err) => console.error('[WS] 错误:', err.message));
  ws.on('close', () => {
    console.warn('[WS] 断开连接,5秒后重连...');
    setTimeout(() => subscribeKlines(symbol, interval, onBar), 5000);
  });

  return ws;
}

Risk manager: flip and loss control

风险管理:翻转与亏损控制

typescript
// src/risk/riskManager.ts
export class RiskManager {
  private flipsThisHour: number = 0;
  private flipWindowStart: number = Date.now();
  private consecutiveLosses: number = 0;

  canFlip(): boolean {
    const now = Date.now();
    if (now - this.flipWindowStart > 3_600_000) {
      this.flipsThisHour = 0;
      this.flipWindowStart = now;
    }
    return this.flipsThisHour < Number(process.env.MAX_FLIPS_PER_HOUR ?? 12);
  }

  recordFlip() {
    this.flipsThisHour++;
  }

  recordTrade(pnl: number) {
    if (pnl < 0) {
      this.consecutiveLosses++;
    } else {
      this.consecutiveLosses = 0;
    }
  }

  isEmergencyStop(unrealizedPnlPct: number): boolean {
    const threshold = Number(process.env.EMERGENCY_STOP_LOSS_PCT ?? 2.0);
    return unrealizedPnlPct <= -threshold;
  }

  positionSize(balanceUsdt: number): number {
    const max = Number(process.env.MAX_POSITION_USDT ?? 10000);
    return Math.min(balanceUsdt * 0.95, max);
  }
}
typescript
// src/risk/riskManager.ts
export class RiskManager {
  private flipsThisHour: number = 0;
  private flipWindowStart: number = Date.now();
  private consecutiveLosses: number = 0;

  canFlip(): boolean {
    const now = Date.now();
    if (now - this.flipWindowStart > 3_600_000) {
      this.flipsThisHour = 0;
      this.flipWindowStart = now;
    }
    return this.flipsThisHour < Number(process.env.MAX_FLIPS_PER_HOUR ?? 12);
  }

  recordFlip() {
    this.flipsThisHour++;
  }

  recordTrade(pnl: number) {
    if (pnl < 0) {
      this.consecutiveLosses++;
    } else {
      this.consecutiveLosses = 0;
    }
  }

  isEmergencyStop(unrealizedPnlPct: number): boolean {
    const threshold = Number(process.env.EMERGENCY_STOP_LOSS_PCT ?? 2.0);
    return unrealizedPnlPct <= -threshold;
  }

  positionSize(balanceUsdt: number): number {
    const max = Number(process.env.MAX_POSITION_USDT ?? 10000);
    return Math.min(balanceUsdt * 0.95, max);
  }
}

Trade logger (CSV + JSON)

交易日志(CSV + JSON)

typescript
// src/lib/logger.ts
import fs from 'fs';
import path from 'path';

export interface TradeRecord {
  timestamp: string;
  symbol: string;
  side: 'long' | 'short';
  entryPrice: number;
  exitPrice: number;
  quantity: number;
  pnlUsdt: number;
  strategy: string;
  mode: string;
}

export function logTrade(trade: TradeRecord): void {
  const date = new Date().toISOString().slice(0, 10);
  const dir = path.join('data', 'trades', 'daily');
  fs.mkdirSync(dir, { recursive: true });

  // JSON log
  const jsonFile = path.join(dir, `${date}.json`);
  const existing: TradeRecord[] = fs.existsSync(jsonFile)
    ? JSON.parse(fs.readFileSync(jsonFile, 'utf-8'))
    : [];
  existing.push(trade);
  fs.writeFileSync(jsonFile, JSON.stringify(existing, null, 2));

  // CSV log
  const csvFile = path.join(dir, `${date}.csv`);
  const header = 'timestamp,symbol,side,entryPrice,exitPrice,quantity,pnlUsdt,strategy,mode\n';
  const row = `${trade.timestamp},${trade.symbol},${trade.side},${trade.entryPrice},` +
              `${trade.exitPrice},${trade.quantity},${trade.pnlUsdt},${trade.strategy},${trade.mode}\n`;
  if (!fs.existsSync(csvFile)) fs.writeFileSync(csvFile, header);
  fs.appendFileSync(csvFile, row);

  console.log(`[TRADE] ${trade.side.toUpperCase()} ${trade.symbol} PnL: ${trade.pnlUsdt.toFixed(2)} USDT`);
}

typescript
// src/lib/logger.ts
import fs from 'fs';
import path from 'path';

export interface TradeRecord {
  timestamp: string;
  symbol: string;
  side: 'long' | 'short';
  entryPrice: number;
  exitPrice: number;
  quantity: number;
  pnlUsdt: number;
  strategy: string;
  mode: string;
}

export function logTrade(trade: TradeRecord): void {
  const date = new Date().toISOString().slice(0, 10);
  const dir = path.join('data', 'trades', 'daily');
  fs.mkdirSync(dir, { recursive: true });

  // JSON日志
  const jsonFile = path.join(dir, `${date}.json`);
  const existing: TradeRecord[] = fs.existsSync(jsonFile)
    ? JSON.parse(fs.readFileSync(jsonFile, 'utf-8'))
    : [];
  existing.push(trade);
  fs.writeFileSync(jsonFile, JSON.stringify(existing, null, 2));

  // CSV日志
  const csvFile = path.join(dir, `${date}.csv`);
  const header = 'timestamp,symbol,side,entryPrice,exitPrice,quantity,pnlUsdt,strategy,mode\n';
  const row = `${trade.timestamp},${trade.symbol},${trade.side},${trade.entryPrice},` +
              `${trade.exitPrice},${trade.quantity},${trade.pnlUsdt},${trade.strategy},${trade.mode}\n`;
  if (!fs.existsSync(csvFile)) fs.writeFileSync(csvFile, header);
  fs.appendFileSync(csvFile, row);

  console.log(`[交易记录] ${trade.side.toUpperCase()} ${trade.symbol} 盈亏: ${trade.pnlUsdt.toFixed(2)} USDT`);
}

Common Patterns

常见配置模式

Starting with safe defaults

安全默认配置

env
MODE=dry-run
MAX_POSITION_USDT=1000
MAX_LEVERAGE=5
MAX_FLIPS_PER_HOUR=6
EMERGENCY_STOP_LOSS_PCT=1.5
REQUIRE_TRENDING_MARKET=true
ADX_THRESHOLD=25
STRATEGY_TYPE=peach-hybrid
VIRTUAL_TIMEFRAME_MS=30000
Always validate in dry-run for at least one full trading session before switching to live.
env
MODE=dry-run
MAX_POSITION_USDT=1000
MAX_LEVERAGE=5
MAX_FLIPS_PER_HOUR=6
EMERGENCY_STOP_LOSS_PCT=1.5
REQUIRE_TRENDING_MARKET=true
ADX_THRESHOLD=25
STRATEGY_TYPE=peach-hybrid
VIRTUAL_TIMEFRAME_MS=30000
在切换到实盘交易前,务必在模拟运行模式下验证至少一个完整交易时段的策略表现。

PM2 deployment

PM2部署

bash
npm install -g pm2
pm2 start npm --name aster-bot -- run bot
pm2 save
pm2 startup
pm2 logs aster-bot
bash
npm install -g pm2
pm2 start npm --name aster-bot -- run bot
pm2 save
pm2 startup
pm2 logs aster-bot

Watching logs

查看日志

bash
undefined
bash
undefined

Live console output

实时控制台输出

pm2 logs aster-bot --lines 100
pm2 logs aster-bot --lines 100

Today's trade log

今日交易日志

cat data/trades/daily/$(date +%Y-%m-%d).json | jq '.'
cat data/trades/daily/$(date +%Y-%m-%d).json | jq '.'

CSV summary

CSV汇总

cat data/trades/daily/$(date +%Y-%m-%d).csv

---
cat data/trades/daily/$(date +%Y-%m-%d).csv

---

Troubleshooting

故障排除

IssueCauseFix
Bot exits immediately at startup
prices().responsive
below 0.871
Set
SKIP_MIN_SPOT_CHECK=true
for testing, or wait for price recovery
TRADING_WALLET_PRIVATE_KEY
error
Key not 64 hex charsCheck key length:
echo -n "$KEY" | wc -c
MAX_LEVERAGE
error
Invalid valueMust be exactly 5, 10, 15, or 50
No signals generatedInsufficient bars for indicatorsWait for
EMA_SLOW
(default 48) bars to accumulate
Orders rejected in live modeAPI key permissionsEnsure futures trading is enabled on AsterDEX account
WebSocket disconnects frequentlyNetwork instabilityBot auto-reconnects after 5s; check VPS network
Strategy never fires in trending modeADX below thresholdLower
ADX_THRESHOLD
or set
REQUIRE_TRENDING_MARKET=false
Too many flipsVolatile market + tight thresholdsReduce
MAX_FLIPS_PER_HOUR
or widen RSI bands
问题原因解决方法
机器人启动后立即退出
prices().responsive
值低于0.871
测试时设置
SKIP_MIN_SPOT_CHECK=true
,或等待价格回升
TRADING_WALLET_PRIVATE_KEY
错误
密钥不是64位十六进制字符检查密钥长度:
echo -n "$KEY" | wc -c
MAX_LEVERAGE
错误
无效值必须是5、10、15或50中的一个
无信号生成指标所需K线数量不足等待
EMA_SLOW
(默认48)根K线积累完成
实盘模式下单被拒绝API密钥权限不足确保AsterDEX账户已开启永续合约交易权限
WebSocket频繁断开网络不稳定机器人会在5秒后自动重连;检查VPS网络状况
趋势模式下策略从未触发ADX值低于阈值降低
ADX_THRESHOLD
或设置
REQUIRE_TRENDING_MARKET=false
翻转次数过多市场波动剧烈且阈值过严减少
MAX_FLIPS_PER_HOUR
或放宽RSI区间

Validating configuration before live run

实盘运行前验证配置

typescript
// Quick config sanity check script
import { config } from './src/config';

const checks = [
  { ok: !!config.apiKey, msg: 'ASTER_API_KEY is set' },
  { ok: !!config.apiSecret, msg: 'ASTER_API_SECRET is set' },
  { ok: config.privateKey?.length === 64, msg: 'Private key is 64 chars' },
  { ok: [5, 10, 15, 50].includes(config.maxLeverage), msg: 'Leverage is valid' },
  { ok: config.maxPositionUsdt > 0, msg: 'MAX_POSITION_USDT > 0' },
  { ok: config.mode === 'dry-run', msg: 'Starting in dry-run mode' },
];

checks.forEach(({ ok, msg }) => {
  console.log(`${ok ? '✓' : '✗'} ${msg}`);
});

typescript
// 快速配置检查脚本
import { config } from './src/config';

const checks = [
  { ok: !!config.apiKey, msg: 'ASTER_API_KEY已设置' },
  { ok: !!config.apiSecret, msg: 'ASTER_API_SECRET已设置' },
  { ok: config.privateKey?.length === 64, msg: '私钥为64位字符' },
  { ok: [5, 10, 15, 50].includes(config.maxLeverage), msg: '杠杆倍数有效' },
  { ok: config.maxPositionUsdt > 0, msg: 'MAX_POSITION_USDT > 0' },
  { ok: config.mode === 'dry-run', msg: '当前为模拟运行模式' },
];

checks.forEach(({ ok, msg }) => {
  console.log(`${ok ? '✓' : '✗'} ${msg}`);
});

Important Notes

重要提示

  • Dry-run first: Always validate strategy behavior in
    MODE=dry-run
    before live trading.
  • Leverage risk:
    MAX_LEVERAGE=50
    means 50x amplified losses. Start with 5.
  • Price gate: The
    web3.prc
    startup check (
    limitPrice = 0.871
    ) prevents trading when ASTER price is too low. Only bypass with
    SKIP_MIN_SPOT_CHECK=true
    in non-production.
  • API endpoint: All REST calls go to
    https://fapi.asterdex.com
    ; WebSocket to
    wss://fstream.asterdex.com/ws
    .
  • State persistence: Bot state survives restarts via
    data/
    directory — do not delete between sessions if you have open positions.
  • Valid leverages: Only
    5
    ,
    10
    ,
    15
    ,
    50
    are accepted by AsterDEX; any other value throws at startup.
  • 先模拟运行:在实盘交易前,务必在
    MODE=dry-run
    模式下验证策略表现。
  • 杠杆风险
    MAX_LEVERAGE=50
    意味着亏损会放大50倍。建议从5倍杠杆开始。
  • 价格防护
    web3.prc
    启动检查(
    limitPrice = 0.871
    )会防止在ASTER价格过低时交易。仅在非生产环境下使用
    SKIP_MIN_SPOT_CHECK=true
    绕过该检查。
  • API端点:所有REST请求发送至
    https://fapi.asterdex.com
    ;WebSocket连接至
    wss://fstream.asterdex.com/ws
  • 状态持久化:机器人状态通过
    data/
    目录保留——如果有未平仓仓位,请勿在重启间删除该目录。
  • 有效杠杆倍数:AsterDEX仅接受
    5
    10
    15
    50
    这几个值;其他值会在启动时抛出错误。