Agent skill

trading-bot-development

Architecture patterns, Discord integration, data management, and best practices for building trading bots that implement strategies like ICT and AMT

Stars 2
Forks 0

Install this agent skill to your Project

npx add-skill https://github.com/Nice-Wolf-Studio/wolf-skills-marketplace/tree/main/trading-bot-development

SKILL.md

Trading Bot Development

Overview

Building a trading bot requires thoughtful architecture to separate concerns, manage data efficiently, and present analysis clearly. This skill provides proven patterns for developing Discord-based trading bots that implement strategies like ICT and AMT.

Use this skill when:

  • Architecting a new trading bot from scratch
  • Adding strategy modules to an existing bot
  • Integrating Discord for user interaction
  • Managing market data (caching, fetching, updating)
  • Implementing multi-strategy orchestration
  • Setting up risk management and position sizing

Tech Stack Assumed:

  • Node.js + TypeScript
  • discord.js (v14+)
  • Databento for market data (futures)
  • SQLite or PostgreSQL for data persistence

Bot Architecture Patterns

1. Separation of Concerns

Core Principle: Separate data fetching, strategy logic, risk management, and presentation into distinct modules.

Directory Structure:

trading-bot/
├── src/
│   ├── commands/          # Discord slash commands
│   ├── strategies/        # Trading strategy implementations
│   │   ├── ict.ts
│   │   ├── amt.ts
│   │   └── index.ts
│   ├── data/              # Data fetching and caching
│   │   ├── databento.ts
│   │   ├── cache.ts
│   │   └── index.ts
│   ├── analysis/          # Strategy orchestration
│   │   ├── signal-generator.ts
│   │   ├── multi-strategy.ts
│   │   └── index.ts
│   ├── risk/              # Risk management
│   │   ├── position-sizing.ts
│   │   ├── risk-calculator.ts
│   │   └── index.ts
│   ├── utils/             # Shared utilities
│   │   ├── types.ts
│   │   ├── timeframes.ts
│   │   └── logger.ts
│   ├── bot.ts             # Discord bot setup
│   └── index.ts           # Entry point
├── data/                  # Cached market data
│   └── cache/
├── config/                # Configuration
│   └── config.ts
├── tests/                 # Unit and integration tests
└── package.json

2. Signal Generation Pipeline

Flow: Data → Preprocessing → Strategy Analysis → Signal Generation → Risk Validation → Output

TypeScript Interface:

typescript
// Common types across all strategies
interface Candle {
  timestamp: number;
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number;
}

interface Signal {
  strategy: string; // 'ict' | 'amt' | 'combined'
  symbol: string;
  timeframe: string;
  direction: 'long' | 'short' | 'neutral';
  entry: number;
  stopLoss: number;
  takeProfit: number;
  confidence: number; // 0-1
  reasoning: string[];
  timestamp: Date;
  metadata?: Record<string, any>;
}

interface StrategyInterface {
  name: string;
  analyze(candles: Candle[], context?: any): Promise<Signal | null>;
  getRequiredDataPoints(): number; // Min candles needed
  getSupportedTimeframes(): string[];
}

Base Strategy Class:

typescript
// src/strategies/base-strategy.ts
export abstract class BaseStrategy implements StrategyInterface {
  abstract name: string;

  abstract analyze(candles: Candle[], context?: any): Promise<Signal | null>;

  abstract getRequiredDataPoints(): number;

  abstract getSupportedTimeframes(): string[];

  protected createSignal(
    symbol: string,
    timeframe: string,
    direction: 'long' | 'short' | 'neutral',
    entry: number,
    stopLoss: number,
    takeProfit: number,
    confidence: number,
    reasoning: string[],
    metadata?: Record<string, any>
  ): Signal {
    return {
      strategy: this.name,
      symbol,
      timeframe,
      direction,
      entry,
      stopLoss,
      takeProfit,
      confidence,
      reasoning,
      timestamp: new Date(),
      metadata
    };
  }

  protected calculateRiskReward(entry: number, stopLoss: number, takeProfit: number): number {
    const risk = Math.abs(entry - stopLoss);
    const reward = Math.abs(takeProfit - entry);
    return reward / risk;
  }
}

Example ICT Strategy Implementation:

typescript
// src/strategies/ict.ts
import { BaseStrategy } from './base-strategy';
import { Candle, Signal } from '../utils/types';
import {
  detectFairValueGaps,
  findLiquidityPools,
  getCurrentKillzone,
  analyzeMarketStructure
} from './ict-indicators'; // Import from ict-strategy skill

export class ICTStrategy extends BaseStrategy {
  name = 'ict';

  getRequiredDataPoints(): number {
    return 100; // Need enough data for swing analysis
  }

  getSupportedTimeframes(): string[] {
    return ['5m', '15m', '1h'];
  }

  async analyze(candles: Candle[], context?: any): Promise<Signal | null> {
    if (candles.length < this.getRequiredDataPoints()) {
      throw new Error(`ICT strategy requires at least ${this.getRequiredDataPoints()} candles`);
    }

    // 1. Check killzone
    const killzone = getCurrentKillzone(new Date(), context?.killzoneConfig);
    if (killzone !== 'NY_AM' && killzone !== 'LONDON_OPEN') {
      return null; // Not in favorable time window
    }

    // 2. Analyze market structure
    const { trend } = analyzeMarketStructure(candles);

    // 3. Detect patterns
    const fvgs = detectFairValueGaps(candles);
    const pools = findLiquidityPools(candles);

    // 4. Check for liquidity sweep
    const recentSwept = pools.filter(p => p.swept);
    if (recentSwept.length === 0) return null;

    // 5. Find entry setup
    const currentPrice = candles[candles.length - 1].close;
    const symbol = context?.symbol || 'UNKNOWN';
    const timeframe = context?.timeframe || '15m';

    // Bullish setup example
    if (trend === 'UPTREND' && recentSwept.some(p => p.type === 'sell_side')) {
      const nearestFVG = fvgs.find(f => f.type === 'bullish' && !f.filled);

      if (nearestFVG) {
        const entry = nearestFVG.gapLow + (nearestFVG.gapHigh - nearestFVG.gapLow) * 0.5;
        const stopLoss = nearestFVG.gapLow - (nearestFVG.gapHigh - nearestFVG.gapLow);
        const takeProfit = entry + (entry - stopLoss) * 2; // 2:1 RR

        return this.createSignal(
          symbol,
          timeframe,
          'long',
          entry,
          stopLoss,
          takeProfit,
          0.75,
          [
            'Uptrend confirmed',
            'Sell-side liquidity swept',
            `In ${killzone} killzone`,
            'Bullish FVG available for entry'
          ],
          {
            killzone,
            fvgCount: fvgs.length,
            liquidityPoolsSwept: recentSwept.length
          }
        );
      }
    }

    // Bearish setup (similar logic)
    // ...

    return null;
  }
}

Multi-Strategy Orchestration

Strategy Manager Pattern

Purpose: Run multiple strategies simultaneously and synthesize their signals.

TypeScript Implementation:

typescript
// src/analysis/multi-strategy.ts
import { Signal } from '../utils/types';
import { BaseStrategy } from '../strategies/base-strategy';

export interface MultiStrategyResult {
  signals: Signal[];
  consensus: 'long' | 'short' | 'neutral' | 'conflicting';
  confidence: number;
  recommendation: string;
}

export class StrategyOrchestrator {
  private strategies: BaseStrategy[] = [];

  registerStrategy(strategy: BaseStrategy): void {
    this.strategies.push(strategy);
  }

  async analyzeAll(
    symbol: string,
    timeframe: string,
    candles: Candle[]
  ): Promise<MultiStrategyResult> {
    const signals: Signal[] = [];

    // Run all strategies in parallel
    const results = await Promise.allSettled(
      this.strategies.map(strategy =>
        strategy.analyze(candles, { symbol, timeframe })
      )
    );

    // Collect successful signals
    results.forEach((result, index) => {
      if (result.status === 'fulfilled' && result.value !== null) {
        signals.push(result.value);
      } else if (result.status === 'rejected') {
        console.error(`Strategy ${this.strategies[index].name} failed:`, result.reason);
      }
    });

    // Synthesize signals
    return this.synthesizeSignals(signals);
  }

  private synthesizeSignals(signals: Signal[]): MultiStrategyResult {
    if (signals.length === 0) {
      return {
        signals: [],
        consensus: 'neutral',
        confidence: 0,
        recommendation: 'No clear setup detected by any strategy'
      };
    }

    // Count directions
    const longCount = signals.filter(s => s.direction === 'long').length;
    const shortCount = signals.filter(s => s.direction === 'short').length;
    const neutralCount = signals.filter(s => s.direction === 'neutral').length;

    // Determine consensus
    let consensus: 'long' | 'short' | 'neutral' | 'conflicting';
    let confidence: number;
    let recommendation: string;

    if (longCount > 0 && shortCount === 0) {
      consensus = 'long';
      confidence = signals.reduce((sum, s) => sum + s.confidence, 0) / signals.length;
      recommendation = `${longCount} strateg${longCount > 1 ? 'ies' : 'y'} signal LONG`;
    } else if (shortCount > 0 && longCount === 0) {
      consensus = 'short';
      confidence = signals.reduce((sum, s) => sum + s.confidence, 0) / signals.length;
      recommendation = `${shortCount} strateg${shortCount > 1 ? 'ies' : 'y'} signal SHORT`;
    } else if (longCount > 0 && shortCount > 0) {
      consensus = 'conflicting';
      confidence = 0.3;
      recommendation = 'Conflicting signals - suggest staying out or waiting for clarity';
    } else {
      consensus = 'neutral';
      confidence = 0;
      recommendation = 'No directional setups detected';
    }

    return { signals, consensus, confidence, recommendation };
  }

  // Weighted voting (more advanced)
  private synthesizeWeighted(signals: Signal[]): MultiStrategyResult {
    if (signals.length === 0) {
      return {
        signals: [],
        consensus: 'neutral',
        confidence: 0,
        recommendation: 'No signals'
      };
    }

    // Weight by confidence
    let longScore = 0;
    let shortScore = 0;

    signals.forEach(signal => {
      if (signal.direction === 'long') {
        longScore += signal.confidence;
      } else if (signal.direction === 'short') {
        shortScore += signal.confidence;
      }
    });

    const totalScore = longScore + shortScore;

    if (totalScore === 0) {
      return {
        signals,
        consensus: 'neutral',
        confidence: 0,
        recommendation: 'No directional conviction'
      };
    }

    if (longScore > shortScore * 1.5) { // Long dominates
      return {
        signals,
        consensus: 'long',
        confidence: longScore / totalScore,
        recommendation: `Strong LONG bias (score: ${longScore.toFixed(2)} vs ${shortScore.toFixed(2)})`
      };
    } else if (shortScore > longScore * 1.5) { // Short dominates
      return {
        signals,
        consensus: 'short',
        confidence: shortScore / totalScore,
        recommendation: `Strong SHORT bias (score: ${shortScore.toFixed(2)} vs ${longScore.toFixed(2)})`
      };
    } else {
      return {
        signals,
        consensus: 'conflicting',
        confidence: Math.abs(longScore - shortScore) / totalScore,
        recommendation: 'Mixed signals - proceed with caution or wait'
      };
    }
  }
}

Discord Integration

Slash Commands

Command Structure:

typescript
// src/commands/analyze.ts
import { SlashCommandBuilder, CommandInteraction, EmbedBuilder } from 'discord.js';
import { StrategyOrchestrator } from '../analysis/multi-strategy';
import { DataManager } from '../data/data-manager';

export const analyzeCommand = {
  data: new SlashCommandBuilder()
    .setName('analyze')
    .setDescription('Analyze market using all strategies')
    .addStringOption(option =>
      option.setName('symbol')
        .setDescription('Symbol (ES, NQ, etc.)')
        .setRequired(true)
        .addChoices(
          { name: 'ES (E-mini S&P 500)', value: 'ES' },
          { name: 'NQ (E-mini Nasdaq)', value: 'NQ' }
        ))
    .addStringOption(option =>
      option.setName('timeframe')
        .setDescription('Timeframe')
        .setRequired(true)
        .addChoices(
          { name: '5 minutes', value: '5m' },
          { name: '15 minutes', value: '15m' },
          { name: '1 hour', value: '1h' }
        )),

  async execute(interaction: CommandInteraction, orchestrator: StrategyOrchestrator, dataManager: DataManager) {
    const symbol = interaction.options.get('symbol')?.value as string;
    const timeframe = interaction.options.get('timeframe')?.value as string;

    await interaction.deferReply(); // Important for long-running analysis

    try {
      // 1. Fetch data
      const candles = await dataManager.getCandles(symbol, timeframe, 200);

      // 2. Run analysis
      const result = await orchestrator.analyzeAll(symbol, timeframe, candles);

      // 3. Build embed
      const embed = new EmbedBuilder()
        .setTitle(`📈 Market Analysis: ${symbol}`)
        .setColor(getColorForConsensus(result.consensus))
        .addFields([
          {
            name: 'Consensus',
            value: `**${result.consensus.toUpperCase()}**`,
            inline: true
          },
          {
            name: 'Confidence',
            value: `${(result.confidence * 100).toFixed(0)}%`,
            inline: true
          },
          {
            name: 'Strategies',
            value: `${result.signals.length} active`,
            inline: true
          }
        ]);

      // Add individual strategy signals
      result.signals.forEach(signal => {
        embed.addFields([{
          name: `${signal.strategy.toUpperCase()} Strategy`,
          value: [
            `Direction: **${signal.direction.toUpperCase()}**`,
            `Entry: ${signal.entry.toFixed(2)}`,
            `Stop: ${signal.stopLoss.toFixed(2)}`,
            `Target: ${signal.takeProfit.toFixed(2)}`,
            `R/R: ${((signal.takeProfit - signal.entry) / Math.abs(signal.entry - signal.stopLoss)).toFixed(2)}`,
            `Reasoning: ${signal.reasoning.join(', ')}`
          ].join('\n'),
          inline: false
        }]);
      });

      embed.addFields([{
        name: 'Recommendation',
        value: result.recommendation,
        inline: false
      }]);

      embed.setTimestamp();
      embed.setFooter({ text: `Timeframe: ${timeframe}` });

      await interaction.editReply({ embeds: [embed] });

    } catch (error) {
      console.error('Analysis error:', error);
      await interaction.editReply({
        content: `Error analyzing ${symbol}: ${error.message}`
      });
    }
  }
};

function getColorForConsensus(consensus: string): number {
  switch (consensus) {
    case 'long': return 0x00FF00; // Green
    case 'short': return 0xFF0000; // Red
    case 'conflicting': return 0xFFA500; // Orange
    default: return 0x808080; // Gray
  }
}

Automated Alerts:

typescript
// src/bot.ts
import { Client, TextChannel } from 'discord.js';

export class TradingBot {
  private client: Client;
  private alertChannelId: string;
  private orchestrator: StrategyOrchestrator;
  private dataManager: DataManager;

  constructor(config: BotConfig) {
    this.client = new Client({ intents: [...] });
    this.alertChannelId = config.alertChannelId;
    this.orchestrator = new StrategyOrchestrator();
    this.dataManager = new DataManager(config.databentoApiKey);
  }

  async startMonitoring(symbols: string[], timeframe: string, intervalMinutes: number) {
    setInterval(async () => {
      for (const symbol of symbols) {
        try {
          const candles = await this.dataManager.getCandles(symbol, timeframe, 200);
          const result = await this.orchestrator.analyzeAll(symbol, timeframe, candles);

          // Only alert on high-confidence signals
          if (result.confidence > 0.7 && result.consensus !== 'neutral') {
            await this.sendAlert(symbol, result);
          }
        } catch (error) {
          console.error(`Monitoring error for ${symbol}:`, error);
        }
      }
    }, intervalMinutes * 60 * 1000);
  }

  private async sendAlert(symbol: string, result: MultiStrategyResult) {
    const channel = this.client.channels.cache.get(this.alertChannelId) as TextChannel;

    if (!channel) {
      console.error('Alert channel not found');
      return;
    }

    const embed = new EmbedBuilder()
      .setTitle(`🚨 Trade Alert: ${symbol}`)
      .setColor(result.consensus === 'long' ? 0x00FF00 : 0xFF0000)
      .setDescription(result.recommendation)
      .addFields(
        result.signals.map(signal => ({
          name: signal.strategy.toUpperCase(),
          value: `${signal.direction.toUpperCase()} @ ${signal.entry.toFixed(2)}`,
          inline: true
        }))
      )
      .setTimestamp();

    await channel.send({ content: '@everyone', embeds: [embed] });
  }
}

Data Management

Databento Integration with Caching

Cache Strategy: Check local cache first, fetch from Databento if missing or stale.

Implementation:

typescript
// src/data/data-manager.ts
import { Candle } from '../utils/types';
import fs from 'fs/promises';
import path from 'path';

export class DataManager {
  private cacheDir: string;
  private databentoApiKey: string;

  constructor(databentoApiKey: string, cacheDir: string = './data/cache') {
    this.databentoApiKey = databentoApiKey;
    this.cacheDir = cacheDir;
  }

  async getCandles(symbol: string, timeframe: string, count: number): Promise<Candle[]> {
    // 1. Check cache
    const cacheKey = `${symbol}_${timeframe}_${count}`;
    const cached = await this.loadFromCache(cacheKey);

    if (cached && this.isCacheFresh(cached, timeframe)) {
      console.log(`Cache hit for ${cacheKey}`);
      return cached.candles;
    }

    // 2. Fetch from Databento
    console.log(`Cache miss for ${cacheKey}, fetching from Databento...`);
    const candles = await this.fetchFromDatabento(symbol, timeframe, count);

    // 3. Save to cache
    await this.saveToCache(cacheKey, candles);

    return candles;
  }

  private async fetchFromDatabento(
    symbol: string,
    timeframe: string,
    count: number
  ): Promise<Candle[]> {
    // Use Databento MCP tools
    // Example: mcp__databento__get_historical_bars
    const response = await this.callDatabentoMCP(symbol, timeframe, count);

    return response.bars.map(bar => ({
      timestamp: new Date(bar.ts_event).getTime(),
      open: bar.open / 1e9, // Databento uses nanosecond prices
      high: bar.high / 1e9,
      low: bar.low / 1e9,
      close: bar.close / 1e9,
      volume: bar.volume
    }));
  }

  private async loadFromCache(key: string): Promise<{ candles: Candle[]; timestamp: number } | null> {
    const cacheFile = path.join(this.cacheDir, `${key}.json`);

    try {
      const data = await fs.readFile(cacheFile, 'utf-8');
      return JSON.parse(data);
    } catch (error) {
      return null; // Cache miss
    }
  }

  private async saveToCache(key: string, candles: Candle[]): Promise<void> {
    const cacheFile = path.join(this.cacheDir, `${key}.json`);

    await fs.mkdir(this.cacheDir, { recursive: true });
    await fs.writeFile(
      cacheFile,
      JSON.stringify({ candles, timestamp: Date.now() }, null, 2)
    );
  }

  private isCacheFresh(cached: { timestamp: number }, timeframe: string): boolean {
    const now = Date.now();
    const age = now - cached.timestamp;

    // Cache expiration based on timeframe
    const expirations = {
      '1m': 1 * 60 * 1000,    // 1 minute
      '5m': 5 * 60 * 1000,    // 5 minutes
      '15m': 15 * 60 * 1000,  // 15 minutes
      '1h': 60 * 60 * 1000,   // 1 hour
      '1d': 24 * 60 * 60 * 1000 // 1 day
    };

    const expiration = expirations[timeframe as keyof typeof expirations] || 15 * 60 * 1000;

    return age < expiration;
  }

  private async callDatabentoMCP(symbol: string, timeframe: string, count: number): Promise<any> {
    // Placeholder - actual implementation uses Databento MCP tools
    // See databento skill for details
    throw new Error('Implement Databento MCP integration');
  }
}

Risk Management

Position Sizing

Fixed Risk Per Trade:

typescript
// src/risk/position-sizing.ts
export interface PositionSizeCalculation {
  contracts: number;
  riskAmount: number;
  potentialLoss: number;
  potentialProfit: number;
}

export class RiskManager {
  private accountBalance: number;
  private riskPercentPerTrade: number; // e.g., 0.01 = 1%

  constructor(accountBalance: number, riskPercentPerTrade: number = 0.01) {
    this.accountBalance = accountBalance;
    this.riskPercentPerTrade = riskPercentPerTrade;
  }

  calculatePositionSize(
    entry: number,
    stopLoss: number,
    pointValue: number = 50 // ES = $50/point, NQ = $20/point
  ): PositionSizeCalculation {
    const riskAmount = this.accountBalance * this.riskPercentPerTrade;
    const riskPerContract = Math.abs(entry - stopLoss) * pointValue;
    const contracts = Math.floor(riskAmount / riskPerContract);

    return {
      contracts: Math.max(contracts, 1), // At least 1 contract
      riskAmount,
      potentialLoss: riskPerContract * contracts,
      potentialProfit: 0 // Calculate based on take profit
    };
  }

  validateRisk(signal: Signal, pointValue: number): boolean {
    const position = this.calculatePositionSize(signal.entry, signal.stopLoss, pointValue);

    // Max 2% risk per trade
    if (position.potentialLoss > this.accountBalance * 0.02) {
      return false;
    }

    // Min 1.5:1 risk/reward ratio
    const rr = Math.abs(signal.takeProfit - signal.entry) / Math.abs(signal.entry - signal.stopLoss);
    if (rr < 1.5) {
      return false;
    }

    return true;
  }
}

Testing Strategies

Unit Testing Strategy Logic

typescript
// tests/strategies/ict.test.ts
import { ICTStrategy } from '../../src/strategies/ict';
import { Candle } from '../../src/utils/types';

describe('ICT Strategy', () => {
  let strategy: ICTStrategy;

  beforeEach(() => {
    strategy = new ICTStrategy();
  });

  it('should detect bullish FVG setup', async () => {
    const candles: Candle[] = generateMockCandles({
      trend: 'up',
      hasFVG: true,
      fvgType: 'bullish'
    });

    const signal = await strategy.analyze(candles, {
      symbol: 'ES',
      timeframe: '15m',
      killzoneConfig: { /* ... */ }
    });

    expect(signal).not.toBeNull();
    expect(signal?.direction).toBe('long');
    expect(signal?.confidence).toBeGreaterThan(0.5);
  });

  it('should return null outside killzone', async () => {
    const candles = generateMockCandles({ trend: 'up', hasFVG: true });

    // Mock time outside killzone
    jest.spyOn(Date, 'now').mockReturnValue(
      new Date('2025-01-15T06:00:00Z').getTime() // 1 AM EST
    );

    const signal = await strategy.analyze(candles);

    expect(signal).toBeNull();
  });
});

function generateMockCandles(config: {
  trend?: 'up' | 'down';
  hasFVG?: boolean;
  fvgType?: 'bullish' | 'bearish';
}): Candle[] {
  // Generate realistic test data
  // ...
}

After Using This Skill

Next steps for your Discord trading bot:

  1. Set up Discord bot and register slash commands
  2. Implement data fetching with Databento MCP integration (see databento skill)
  3. Add strategy modules using ict-strategy and amt-strategy skills
  4. Test with historical data before going live
  5. Add logging and error handling
  6. Consider adding backtesting module

For production deployment:

  • Use environment variables for API keys
  • Set up proper logging (Winston, Pino)
  • Add rate limiting for Discord commands
  • Monitor bot health and uptime
  • Consider using PM2 or Docker for process management

Last Updated: January 2025 Version: 1.0.0 Part of Wolf Skills Marketplace

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results