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
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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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
// 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:
- Set up Discord bot and register slash commands
- Implement data fetching with Databento MCP integration (see
databentoskill) - Add strategy modules using
ict-strategyandamt-strategyskills - Test with historical data before going live
- Add logging and error handling
- 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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
daily-summary
Use when preparing daily standups or status reports - automates PR summary generation with categorization, metrics, and velocity analysis; eliminates manual report compilation and ensures consistent format
wolf-scripts-core
Core automation scripts for archetype selection, evidence validation, quality scoring, and safe bash execution
wolf
Master skill for Wolf Agents institutional knowledge and behavioral patterns (v1.1.0 with skill-chaining)
wolf-archetypes
Behavioral archetypes for automatic agent adaptation based on work type
ict-strategy
Inner Circle Trader (ICT) methodology - smart money concepts, liquidity pools, fair value gaps, order blocks, and market structure for trading bot implementation
databento
Use when working with ES/NQ futures market data, before calling any Databento API - follow mandatory four-step workflow (cost check, availability check, fetch, validate); prevents costly API errors and ensures data quality
Didn't find tool you were looking for?