Zero-touch automation — the complete daily pipeline that scans markets, scores setups, and delivers A++ recommendations to your Discord every single day.
Here's what your day looks like once the pipeline is running:
No charts to analyze. No screeners to run. No articles to write. The system does everything. You just follow the highest-conviction alerts.
The bot is a Node.js application using discord.js v14. It's the bridge between Claude Code and your Discord server.
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
const { exec } = require('child_process');
const schedule = require('./scheduler');
const config = require('./config.json');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
// Command handler
client.on('messageCreate', async (message) => {
if (message.author.bot) return;
if (!message.content.startsWith(config.prefix)) return;
const args = message.content.slice(config.prefix.length).trim().split(/ +/);
const command = args.shift().toLowerCase();
switch (command) {
case 'analyze':
await handleAnalyze(message, args);
break;
case 'scan':
await handleScan(message);
break;
case 'market':
await handleMarketOverview(message);
break;
case 'alerts':
await handleAlerts(message, args);
break;
case 'status':
await handleStatus(message);
break;
case 'schedules':
await handleSchedules(message);
break;
case 'run':
await handleManualRun(message, args);
break;
case 'pause':
case 'resume':
case 'cancel':
await handleScheduleControl(message, command, args);
break;
}
});
// How the bot invokes Claude Code
async function runClaudeCode(project, prompt) {
return new Promise((resolve, reject) => {
const cmd = `claude -p "${prompt}" --project /path/to/${project}`;
const proc = exec(cmd, {
maxBuffer: 10 * 1024 * 1024, // 10MB for large outputs
timeout: 600000 // 10 min timeout
});
let output = '';
proc.stdout.on('data', (data) => output += data);
proc.stderr.on('data', (data) => console.error(`[stderr] ${data}`));
proc.on('close', (code) => {
if (code === 0) resolve(output);
else reject(new Error(`Claude Code exited with code ${code}`));
});
});
}
// Scheduled task execution
async function executeScheduledTask(task) {
const channel = client.channels.cache.get(config.channels.botLog);
try {
await channel?.send(`⏰ Starting scheduled task: **${task.command}**`);
const result = await runClaudeCode(task.project, task.command);
await channel?.send(`✅ Task completed: **${task.command}**`);
// Post results to appropriate channel based on task type
if (task.command.includes('daily')) {
await postDailyBriefing(result);
} else if (task.command.includes('scan')) {
await postScannerResults(result);
} else if (task.command.includes('retrospective')) {
await postRetrospective(result);
}
} catch (error) {
await channel?.send(`❌ Task failed: **${task.command}**\n\`\`\`${error.message}\`\`\``);
}
}
client.login(config.token);
module.exports = {
apps: [{
name: 'claude-discord-bot',
script: './bot.js',
watch: false,
autorestart: true,
max_restarts: 10,
restart_delay: 5000,
env: {
NODE_ENV: 'production',
DISCORD_TOKEN: process.env.DISCORD_TOKEN
}
}]
};
// Start: pm2 start ecosystem.config.js
// Monitor: pm2 monit
// Logs: pm2 logs claude-discord-bot
function buildSetupEmbed(setup) {
const color = setup.grade === "A++" ? 0xf59e0b :
setup.grade === "A+" ? 0x10b981 : 0x3b82f6;
const emoji = setup.direction === "long" ? "🟢" : "🔴";
return new EmbedBuilder()
.setColor(color)
.setTitle(`${emoji} CONFIRMED: ${setup.ticker} — ${setup.strategy} [${setup.grade}]`)
.setDescription(setup.thesis)
.addFields(
{ name: 'Score', value: `${setup.composite}/100`, inline: true },
{ name: 'Entry', value: `$${setup.entry}`, inline: true },
{ name: 'Stop', value: `$${setup.stop} (${setup.stopPct}%)`, inline: true },
{ name: 'Target 1', value: `$${setup.tp1} (+${setup.tp1Pct}%)`, inline: true },
{ name: 'Target 2', value: `$${setup.tp2} (+${setup.tp2Pct}%)`, inline: true },
{ name: 'R/R', value: `${setup.rr}`, inline: true },
{ name: 'Regime', value: setup.regime, inline: true },
{ name: 'Horizon', value: setup.horizon, inline: true },
{ name: 'Sector', value: `${setup.geo} ${setup.sector}`, inline: true }
)
.setThumbnail(`https://charts2.finviz.com/chart.ashx?t=${setup.ticker}&ty=c&ta=1&p=d&s=l`)
.setFooter({ text: `DailyTickers Scanner | ${setup.date} | Confluence: ${setup.confluence}` })
.setTimestamp();
}
[
{
"id": 1,
"name": "Daily Briefing",
"schedule": "every day at 07:00",
"command": "analyse daily",
"project": "articles",
"enabled": true,
"timezone": "Europe/Paris",
"channels": ["#market-briefing"],
"lastRun": "2026-03-05T07:00:00Z",
"nextRun": "2026-03-06T07:00:00Z"
},
{
"id": 2,
"name": "Daily Scanner",
"schedule": "every weekday at 23:00",
"command": "scan du jour",
"project": "articles",
"enabled": true,
"timezone": "America/New_York",
"channels": ["#trading-signals", "#priority-alerts"],
"lastRun": "2026-03-05T23:00:00Z",
"nextRun": "2026-03-06T23:00:00Z"
},
{
"id": 3,
"name": "Weekly Retrospective",
"schedule": "every friday at 23:00",
"command": "rétrospective scanner",
"project": "articles",
"enabled": true,
"channels": ["#performance"],
"lastRun": "2026-02-28T23:00:00Z",
"nextRun": "2026-03-07T23:00:00Z"
},
{
"id": 4,
"name": "Weekly Report",
"schedule": "every sunday at 18:00",
"command": "nouvelle analyse weekly",
"project": "articles",
"enabled": true,
"channels": ["#weekly-report"]
}
]
// List all schedules
!schedules
→ #1 Daily Briefing | every day at 07:00 | ✅ Enabled | Next: Mar 6 07:00
#2 Daily Scanner | weekday at 23:00 | ✅ Enabled | Next: Mar 6 23:00
#3 Weekly Retro | friday at 23:00 | ✅ Enabled | Next: Mar 7 23:00
#4 Weekly Report | sunday at 18:00 | ✅ Enabled | Next: Mar 8 18:00
// Force-run a task now
!run #2
→ ⏰ Starting: Daily Scanner (manual trigger)
// Pause a task
!pause #2
→ ⏸️ Paused: Daily Scanner
// Resume
!resume #2
→ ▶️ Resumed: Daily Scanner
Here's exactly what happens when the 23:00 scanner task fires:
// Step 1: Read retrospective feedback
const retros = readAllRetrospectives("scanner/retrospective/");
const adjustedWeights = computeAdjustedWeights(retros);
// Output: strategy weights modified by historical performance
// Step 2: Read last 3 scans for anti-duplicate check
const recentTickers = readRecentScans(3);
// Output: Set of tickers to avoid (70% must be new)
// Step 3: Regime detection
const overview = await GetMarketOverview({ depth: "deep" });
const regime = detectRegime(overview);
// Output: "EARLY_RISK_OFF", strategy weights
// Step 4: Multi-strategy screening
const autoResults = await RunAutoScreener();
const oversold = await RunScreener({
conditions: "rsi14 < 30 && vol > sma(vol,20) * 2 && close > sma(close,200)",
universe: "SP500"
});
const momentum = await RunScreener({
conditions: "close > max(high,20d) && vol > avgVol * 1.5 && rsi14 > 55",
universe: "SP500"
});
const breakout = await RunScreener({
conditions: "bbWidth < 5 && close > sma(close,200)",
universe: "SP500"
});
// Output: ~50 unique candidates
// Step 5: EU/APAC/ETF screening
const euApac = await RunScreener({
conditions: "rsi14 < 40 && close > ema(close,50)",
symbols: ["VGK","EWG","EWQ","SAP","ASML","EWJ","EWY","FXI"]
});
const etfs = await RunScreener({
conditions: "change(close,5d) > 2 && vol > avgVol",
symbols: ["XLF","XLE","XLK","XLV","GLD","SLV","TLT","ARKK"]
});
// Output: ~10 additional candidates
// Step 6: Deep data collection
const deepData = await QueryData({
symbols: allCandidates,
types: "quote,technicals,support_resistance,sentiment_overall," +
"insider_transactions,financials,news,trading_signals"
});
// Output: Full data for 60 candidates
// Step 7: P0 validation
const validated = allCandidates.filter(t => {
const spot = deepData[t].quote.price;
const entry = computeEntry(deepData[t]);
return Math.abs(spot - entry) / spot < 0.10; // Within 10%
});
// Output: ~55 validated candidates
// Step 8: Multi-layer scoring (Part 3)
const scored = validated.map(t => scoreAllLayers(t, deepData[t], regime));
// Output: Each candidate has composite score + 6 layer scores
// Step 9: Filter and diversify
const topCandidates = scored
.filter(s => s.composite >= 85 && s.failedLayers.length === 0)
.sort((a, b) => b.composite - a.composite);
const final10 = diversify(topCandidates, {
minUS: 5, minEU: 2, minAPAC: 1, minETF: 2,
minDefensive: regime.includes("RISK_OFF") ? 2 : 0
});
// Output: 10 diversified A+ setups
// Step 10: A++ re-rank (top 3)
const top3 = final10.slice(0, 3);
// These go to #priority-alerts
// Step 11: Generate HTML + deploy
await generateScannerArticle(final10, regime, overview);
await exec("node tools/add_card.js scanner/YYYYMMDD/index.html");
await exec("git add scanner/ data/ && git commit && git push");
// Output: Article live on GitHub Pages
// Step 12: Post to Discord + create alerts
for (const setup of top3) {
await postToChannel("#priority-alerts", buildSetupEmbed(setup));
}
for (const setup of final10) {
await postToChannel("#trading-signals", buildSetupSummary(setup));
await createMonitoringAlert(setup); // Part 4 alert system
}
From 500+ stocks in the universe down to 3 A++ trades — here's the funnel:
| Stage | Filter | Candidates | Reduction |
|---|---|---|---|
| 1. Universe | RunAutoScreener + 3 DSL strategies | ~500 → 60 | 88% |
| 2. P0 Validation | Price within 10% of spot, not halted | 60 → 55 | 8% |
| 3. Multi-Layer Score | Composite ≥ 85, no failed layers | 55 → 20 | 64% |
| 4. Diversification | Sector/geo/strategy mix requirements | 20 → 12 | 40% |
| 5. Anti-Duplicate | 70% must be new vs last 3 scans | 12 → 10 | 17% |
| 6. AI Re-Rank | Top 3 by conviction + R/R + uniqueness | 10 → 3 | 70% |
Retail traders lose money by over-trading. By limiting to 3 A++ setups per day, you force extreme selectivity. Each trade has maximum conviction across all 6 scoring layers. Quality over quantity. If none qualify as A++, the system reports 0 trades — and that's perfectly fine.
📊 Bot Status
━━━━━━━━━━━━━━━━━━━━━━
Uptime: 12d 4h 32m
Last Scan: Mar 5 23:00 ✅
Last Briefing: Mar 6 07:00 ✅
Last Retro: Feb 28 23:00 ✅
MCP Servers:
dailytickers: 🟢 Connected (latency: 45ms)
market-alerts: 🟢 Connected (latency: 12ms)
Active Alerts: 10 watching, 2 near-confirmed, 0 confirmed
Errors (24h): 0
Disk Usage: 2.3 GB / 10 GB
Git Status: clean, up to date with origin/main
| Error Type | Action | Notification |
|---|---|---|
| MCP connection failure | Retry 3x with exponential backoff | #bot-logs warning |
| Claude Code timeout (10min) | Kill process, retry once | #bot-logs error |
| Git push failure | Stash, pull, reapply, retry | #bot-logs error |
| Empty scanner results | Loosen DSL thresholds by 10% | #bot-logs warning |
| HTML < 10KB | Abort push, log error | #bot-logs critical |
| add_card.js failure | Abort push, manual fix needed | #bot-logs critical + @admin |
~/.claude/settings.json, not in repo!run, !pause, !cancel!run per 5 minutes per user.env, config.json, node_modules/