aster-bot-trading
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAster 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.
Installation
安装
bash
git clone https://github.com/SignalBot-Labs/aster-bot.git
cd aster-bot
npm install
cp env.example .env.localEdit with your credentials (see Configuration below), then:
.env.localbash
undefinedbash
git clone https://github.com/SignalBot-Labs/aster-bot.git
cd aster-bot
npm install
cp env.example .env.local编辑文件,填入你的凭证(详见下方配置部分),然后执行:
.env.localbash
undefinedDry-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.localRequired
必填配置
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: liveenv
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 # 可选值:liveRisk 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=25env
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=25Strategy Selection
策略选择
env
STRATEGY_TYPE=peach-hybrid # or: watermellonenv
STRATEGY_TYPE=peach-hybrid # 可选值:watermellonTimeframe
时间框架
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 's at startup and checks the field against in . If below, the bot exits.
web3.prcprices()responsivelimitPrice = 0.871src/lib/spotPrice.tsenv
SKIP_MIN_SPOT_CHECK=true # Skip price gate for local testing only机器人启动时会调用的方法,并在中检查字段是否低于。如果低于该值,机器人将退出运行。
web3.prcprices()src/lib/spotPrice.tsresponsivelimitPrice = 0.871env
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=58Logic:
- Long: bullish EMA stack (fast > mid > slow) + RSI ≥ + ADX ≥
RSI_MIN_LONGADX_THRESHOLD - Short: bearish EMA stack (fast < mid < slow) + RSI ≤ + ADX ≥
RSI_MAX_SHORTADX_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 ≥ + ADX ≥
RSI_MIN_LONGADX_THRESHOLD - 做空: 看跌EMA排列(快线 < 中线 < 慢线) + RSI ≤ + ADX ≥
RSI_MAX_SHORTADX_THRESHOLD
Peach Hybrid (Dual V1 + V2 system)
Peach Hybrid(V1 + V2双系统策略)
env
STRATEGY_TYPE=peach-hybridenv
STRATEGY_TYPE=peach-hybridV1 — 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
undefinedbash
undefinedStart 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.jsonaster-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.jsonReal 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=30000Always 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-botbash
npm install -g pm2
pm2 start npm --name aster-bot -- run bot
pm2 save
pm2 startup
pm2 logs aster-botWatching logs
查看日志
bash
undefinedbash
undefinedLive 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
故障排除
| Issue | Cause | Fix |
|---|---|---|
| Bot exits immediately at startup | | Set |
| Key not 64 hex chars | Check key length: |
| Invalid value | Must be exactly 5, 10, 15, or 50 |
| No signals generated | Insufficient bars for indicators | Wait for |
| Orders rejected in live mode | API key permissions | Ensure futures trading is enabled on AsterDEX account |
| WebSocket disconnects frequently | Network instability | Bot auto-reconnects after 5s; check VPS network |
| Strategy never fires in trending mode | ADX below threshold | Lower |
| Too many flips | Volatile market + tight thresholds | Reduce |
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 机器人启动后立即退出 | | 测试时设置 |
| 密钥不是64位十六进制字符 | 检查密钥长度: |
| 无效值 | 必须是5、10、15或50中的一个 |
| 无信号生成 | 指标所需K线数量不足 | 等待 |
| 实盘模式下单被拒绝 | API密钥权限不足 | 确保AsterDEX账户已开启永续合约交易权限 |
| WebSocket频繁断开 | 网络不稳定 | 机器人会在5秒后自动重连;检查VPS网络状况 |
| 趋势模式下策略从未触发 | ADX值低于阈值 | 降低 |
| 翻转次数过多 | 市场波动剧烈且阈值过严 | 减少 |
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 before live trading.
MODE=dry-run - Leverage risk: means 50x amplified losses. Start with 5.
MAX_LEVERAGE=50 - Price gate: The startup check (
web3.prc) prevents trading when ASTER price is too low. Only bypass withlimitPrice = 0.871in non-production.SKIP_MIN_SPOT_CHECK=true - API endpoint: All REST calls go to ; WebSocket to
https://fapi.asterdex.com.wss://fstream.asterdex.com/ws - State persistence: Bot state survives restarts via directory — do not delete between sessions if you have open positions.
data/ - Valid leverages: Only ,
5,10,15are accepted by AsterDEX; any other value throws at startup.50
- 先模拟运行:在实盘交易前,务必在模式下验证策略表现。
MODE=dry-run - 杠杆风险:意味着亏损会放大50倍。建议从5倍杠杆开始。
MAX_LEVERAGE=50 - 价格防护:启动检查(
web3.prc)会防止在ASTER价格过低时交易。仅在非生产环境下使用limitPrice = 0.871绕过该检查。SKIP_MIN_SPOT_CHECK=true - API端点:所有REST请求发送至;WebSocket连接至
https://fapi.asterdex.com。wss://fstream.asterdex.com/ws - 状态持久化:机器人状态通过目录保留——如果有未平仓仓位,请勿在重启间删除该目录。
data/ - 有效杠杆倍数:AsterDEX仅接受、
5、10、15这几个值;其他值会在启动时抛出错误。50