惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
博客园 - Franky
T
Tailwind CSS Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
月光博客
月光博客
人人都是产品经理
人人都是产品经理
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
C
Check Point Blog
爱范儿
爱范儿
T
The Blog of Author Tim Ferriss
aimingoo的专栏
aimingoo的专栏
Stack Overflow Blog
Stack Overflow Blog
博客园 - 聂微东
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
云风的 BLOG
云风的 BLOG
MyScale Blog
MyScale Blog
Microsoft Security Blog
Microsoft Security Blog
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building a Risk-Aware Crypto Execution Queue in Node.js
Vinicius Chelles · 2026-06-16 · via DEV Community

Building a Risk-Aware Crypto Execution Queue in Node.js

Most crypto bot tutorials stop at the exciting part: fetch candles, calculate an indicator, and send a market order. That is useful for learning, but it is not how a resilient trading system should behave in production. The fragile part is almost always the small gap between "the strategy wants to trade" and "the exchange accepted the order."

That gap is where duplicate signals, partial failures, rate limits, stale prices, and emotional position sizing can quietly turn a reasonable idea into a dangerous bot.

In this tutorial, we will design a risk-aware execution queue in Node.js. The goal is not to predict the market. The goal is to build the boring layer that makes any strategy safer: one path for orders, explicit risk checks, idempotency, retries, and audit logs.

Why use an execution queue?

A trading strategy should decide intent. An execution engine should decide whether that intent is still allowed.

For example, your strategy might emit this:

{
  symbol: "BTC/USDT",
  side: "buy",
  reason: "ema_cross_15m",
  confidence: 0.72
}

That is not an order yet. It is a request. Before it becomes an order, the system should answer a few questions:

  • Is the bot already in a position for this symbol?
  • Is the request too old?
  • Would the order exceed max position size?
  • Is the exchange reachable?
  • Has this exact signal already been processed?
  • Are we inside the daily loss limit?

An execution queue gives you one controlled place to ask those questions.

Project setup

Create a small Node.js project:

mkdir crypto-execution-queue
cd crypto-execution-queue
npm init -y
npm install pino

For this tutorial, we will mock the exchange adapter so the architecture is clear. In a real bot, you can plug in CCXT, a direct exchange SDK, or your own REST/WebSocket client.

Define trade intents

Start with an intent factory. Every request gets a deterministic idempotency key. This prevents one signal from becoming multiple orders if your strategy retries, your process restarts, or a message gets delivered twice.

import crypto from "node:crypto";

export function createTradeIntent({ symbol, side, strategy, timeframe, candleTime, confidence }) {
  const rawKey = `${strategy}:${symbol}:${side}:${timeframe}:${candleTime}`;
  const idempotencyKey = crypto.createHash("sha256").update(rawKey).digest("hex");

  return {
    idempotencyKey,
    symbol,
    side,
    strategy,
    timeframe,
    candleTime,
    confidence,
    createdAt: Date.now()
  };
}

Notice that the key does not include the current timestamp. That is intentional. If the same strategy emits the same signal for the same candle, we want the same key.

Add a risk gate

The risk gate is a pure function that receives current state and a trade intent. It returns an approval or a rejection. Keeping this logic separate makes it much easier to test.

const MAX_SIGNAL_AGE_MS = 60_000;
const MAX_OPEN_POSITIONS = 3;
const MAX_DAILY_LOSS_USDT = 150;
const MAX_CONFIDENCE_ORDER_USDT = 500;
const BASE_ORDER_USDT = 100;

export function evaluateRisk(intent, state) {
  if (Date.now() - intent.createdAt > MAX_SIGNAL_AGE_MS) {
    return { approved: false, reason: "stale_signal" };
  }

  if (state.processedKeys.has(intent.idempotencyKey)) {
    return { approved: false, reason: "duplicate_signal" };
  }

  if (state.dailyPnl <= -MAX_DAILY_LOSS_USDT) {
    return { approved: false, reason: "daily_loss_limit" };
  }

  if (!state.positions.has(intent.symbol) && state.positions.size >= MAX_OPEN_POSITIONS) {
    return { approved: false, reason: "max_open_positions" };
  }

  const confidenceBoost = Math.max(0, intent.confidence - 0.5) * 2;
  const notional = Math.min(
    BASE_ORDER_USDT * (1 + confidenceBoost),
    MAX_CONFIDENCE_ORDER_USDT
  );

  return {
    approved: true,
    notional,
    reason: "approved"
  };
}

This is deliberately simple. The key idea is that risk decisions are explicit and logged. You can later replace these constants with configuration, add volatility-based sizing, or check account margin before approval.

Create the execution queue

Now we need a queue that processes one intent at a time. Serial execution is underrated. Many exchange bugs happen because two asynchronous parts of the bot both believe they are allowed to trade.

import pino from "pino";
import { evaluateRisk } from "./risk.js";

const log = pino({ level: "info" });

export class ExecutionQueue {
  constructor({ exchange, state }) {
    this.exchange = exchange;
    this.state = state;
    this.queue = [];
    this.running = false;
  }

  enqueue(intent) {
    this.queue.push(intent);
    this.run().catch((error) => {
      log.error({ error }, "execution_queue_failed");
    });
  }

  async run() {
    if (this.running) return;
    this.running = true;

    try {
      while (this.queue.length > 0) {
        const intent = this.queue.shift();
        await this.process(intent);
      }
    } finally {
      this.running = false;
    }
  }

  async process(intent) {
    const decision = evaluateRisk(intent, this.state);

    if (!decision.approved) {
      log.warn({ intent, decision }, "trade_rejected");
      this.state.processedKeys.add(intent.idempotencyKey);
      return;
    }

    const ticker = await this.exchange.getTicker(intent.symbol);
    const amount = decision.notional / ticker.price;

    const order = await this.exchange.createMarketOrder({
      symbol: intent.symbol,
      side: intent.side,
      amount,
      clientOrderId: intent.idempotencyKey
    });

    this.state.processedKeys.add(intent.idempotencyKey);
    this.state.positions.set(intent.symbol, {
      symbol: intent.symbol,
      side: intent.side,
      amount,
      entryPrice: ticker.price,
      orderId: order.id
    });

    log.info({ intent, order, amount, price: ticker.price }, "trade_executed");
  }
}

The queue does three important things:

  • It prevents concurrent order placement.
  • It marks duplicate or rejected requests as processed.
  • It uses clientOrderId so the exchange can also help with idempotency when supported.

Mock the exchange adapter

For local testing, use a fake adapter:

export const mockExchange = {
  async getTicker(symbol) {
    return {
      symbol,
      price: 65_000 + Math.random() * 500
    };
  },

  async createMarketOrder({ symbol, side, amount, clientOrderId }) {
    return {
      id: `order_${Date.now()}`,
      symbol,
      side,
      amount,
      clientOrderId,
      status: "filled"
    };
  }
};

This makes the queue easy to test without touching real funds. Once the behavior is correct, the exchange adapter becomes an implementation detail.

Wire everything together

Here is a tiny runnable example:

import { ExecutionQueue } from "./execution-queue.js";
import { createTradeIntent } from "./intent.js";
import { mockExchange } from "./mock-exchange.js";

const state = {
  processedKeys: new Set(),
  positions: new Map(),
  dailyPnl: 0
};

const queue = new ExecutionQueue({
  exchange: mockExchange,
  state
});

const intent = createTradeIntent({
  symbol: "BTC/USDT",
  side: "buy",
  strategy: "ema_cross",
  timeframe: "15m",
  candleTime: "2026-06-16T12:00:00.000Z",
  confidence: 0.72
});

queue.enqueue(intent);
queue.enqueue(intent);

The first intent should execute. The second should be rejected as a duplicate. That is exactly what you want when the market is moving quickly and your system is under pressure.

Production upgrades

Before using this pattern with real capital, add persistence. In-memory Set and Map objects are fine for a tutorial, but production bots should store processed keys, positions, and order events in a database. SQLite is enough for a small single-server bot. Postgres is better when you expect multiple services, dashboards, or analytics.

You should also add retry rules with care. Retrying a failed request is not always safe. A network timeout after order submission does not mean the order failed. It might mean the exchange accepted the order and your client never received the response. The safer flow is:

  1. Submit with a client order id.
  2. On timeout, query the exchange for that client order id.
  3. Only resubmit if the exchange confirms it does not exist.

Finally, add a kill switch. A simple environment-controlled flag, database setting, or admin command can stop new executions while still allowing the bot to read market data and manage existing positions.

Final thoughts

Profitable strategies matter, but execution discipline keeps a bot alive long enough to learn whether the strategy is actually good. A queue-based execution layer gives your Node.js trading system a clean boundary between signal generation and capital movement.

The pattern is small: create intents, evaluate risk, process sequentially, log every decision, and make every order idempotent. That small layer can prevent many of the expensive mistakes that happen when a trading bot grows from a weekend script into a real system.

I am building Lucromatic, a platform for automated crypto trading workflows and execution tooling. Demo: try.lucromatic.com