Automated Trading with Claude Code — Part 4/6

The Alert DSL

Programmable trading triggers with composable conditions, automatic setup confirmation, state machine tracking, and the market-alerts MCP server.

Programmable Alerts Composable DSL Auto-Confirmation State Machine
Automated Trading with Claude Code 4/6
Why a DSL DSL Syntax Templates State Machine MCP Alerts Auto-Confirm DSL Evolution

Why Build a DSL?

Hardcoding alert conditions in scripts means every change requires code edits, testing, and deployment. A DSL gives you:

The market-alerts MCP server provides 7 tools: create_alert, list_alerts, update_alert, delete_alert, pause_alert, snooze_alert, get_alert_status.

Alert DSL Syntax

Condition Types

Price Conditions

Price
price > 150.00
price < sma(200)
price crosses_above ema(50)
price within_range(145, 155)
price at_52w_high
price gap_up > 3%

Volume Conditions

Volume
volume > avg_volume * 2.0
volume_surge > 3x
relative_volume > 2.5
volume_trend == rising(5d)

Technical Indicators

Technical
rsi(14) < 30
rsi(14) crosses_above 30
macd crosses_above signal
macd_histogram > 0
bb_squeeze == true
bb_width < percentile(10)
ema(20) crosses_above ema(50)
atr(14) > atr_avg * 1.5

Fundamental Triggers

Fundamental
earnings_in_days <= 5
pe_ratio < sector_avg * 0.8
insider_buying_90d > 3
revenue_growth_yoy > 20
short_interest > 20%

Sentiment Conditions

Sentiment
stocktwits_bull_ratio > 0.7
reddit_mentions_24h > 100
news_sentiment == positive
social_volume_spike > 3x

Composite Operators

Logic
condition_a AND condition_b
condition_a OR condition_b
NOT condition_a
(a AND b) OR (c AND d)
ALL_OF(cond1, cond2, cond3) // all must be true
ANY_OF(cond1, cond2, cond3) // at least 1 true
N_OF(3, cond1, cond2, cond3, cond4) // 3 out of 4

Time-Based

Time
within_market_hours
premarket
after_hours
day_of_week IN (Mon, Tue, Wed)
not_opex_week

Full Grammar (EBNF)

alert_dsl.ebnf
alert       := condition (("AND" | "OR") condition)*
condition   := "NOT"? atom | "(" alert ")" | quantifier
atom        := field operator value
quantifier  := ("ALL_OF" | "ANY_OF") "(" alert ("," alert)* ")"
             | "N_OF" "(" number "," alert ("," alert)* ")"
field       := "price" | "volume" | "rsi" "(" number ")"
             | "macd" | "signal" | "macd_histogram"
             | "ema" "(" number ")" | "sma" "(" number ")"
             | "bb_width" | "bb_squeeze" | "atr" "(" number ")"
             | "pe_ratio" | "earnings_in_days" | "short_interest"
             | "stocktwits_bull_ratio" | "reddit_mentions_24h"
             | "news_sentiment" | "insider_buying_90d"
             | "relative_volume" | "volume_surge"
operator    := ">" | "<" | ">=" | "<=" | "==" | "!="
             | "crosses_above" | "crosses_below"
             | "within_range" | "IN"
value       := number | string | field | function_call
function_call := ("sma" | "ema" | "avg" | "max" | "min"
                | "percentile" | "sector_avg") "(" args ")"
number      := [0-9]+ ("." [0-9]+)? ("%")?
string      := '"' [^"]* '"'

Alert Templates

Pre-built alert patterns for common trading setups. Each template is a tested combination of conditions that has been validated through backtesting.

Breakout Alert
price crosses_above resistance(1)
AND volume > avg_volume * 1.5
AND rsi(14) > 50
AND rsi(14) < 75
AND within_market_hours
Oversold Bounce
rsi(14) crosses_above 30
AND price > support(1)
AND macd crosses_above signal
AND volume > avg_volume * 1.2
Earnings Play
earnings_in_days <= 2
AND iv_rank > 70
AND put_call_ratio < 0.7
AND NOT bb_squeeze
Squeeze Setup
bb_width < percentile(10)
AND atr(14) < sma(atr(14), 20) * 0.7
AND volume_trend == contracting(5d)
AND price > sma(200)
Mean Reversion
price < sma(200)
AND rsi(14) < 25
AND insider_buying_90d > 0
AND pe_ratio < sector_avg * 0.7
Momentum Continuation
price > ema(20)
AND ema(20) > ema(50)
AND ema(50) > ema(200)
AND volume_trend == rising(5d)
AND rsi(14) > 55 AND rsi(14) < 72
Risk-Off Hedge Trigger
vix crosses_above 25
OR (spy_rsi(14) < 30 AND vix > 20)
OR (dxy_change_5d > 2 AND yield_10y_change_5d > 0.15)
Insider Signal
insider_buying_90d >= 3
AND insider_buy_value > 500000
AND price < sma(200)
AND short_interest < 10%

Setup Confirmation State Machine

Alerts don't just fire once. They track a setup through multiple states from initial detection to confirmation to execution.

Watching
Forming
Near-Confirmed
Confirmed
Triggered
State Machine Logic
function evaluateSetupState(conditions, currentData) {
  const total = conditions.length;
  const met = conditions.filter(c => evaluate(c, currentData)).length;
  const ratio = met / total;

  if (ratio === 0) return "EXPIRED";      // No conditions met
  if (ratio < 0.5) return "WATCHING";      // Less than half
  if (ratio < 0.75) return "FORMING";      // More than half
  if (ratio < 1.0) return "NEAR_CONFIRMED"; // Almost all
  if (ratio === 1.0) return "CONFIRMED";    // All conditions met!

  // After CONFIRMED, wait for entry trigger
  if (state === "CONFIRMED" && entryTriggered(currentData)) {
    return "TRIGGERED"; // Send alert + create trade log
  }
}

// Partial match reporting
function getPartialReport(conditions, currentData) {
  return conditions.map(c => ({
    condition: c.toString(),
    met: evaluate(c, currentData),
    currentValue: getCurrentValue(c.field, currentData),
    targetValue: c.value,
    progress: calculateProgress(c, currentData) // 0-100%
  }));
}

// Example output:
// {
//   state: "NEAR_CONFIRMED",
//   met: 5,
//   total: 6,
//   conditions: [
//     { condition: "rsi(14) < 30", met: true, current: 28.5, target: 30, progress: 100 },
//     { condition: "vol > avg*2", met: true, current: 2.3, target: 2.0, progress: 100 },
//     { condition: "macd > signal", met: false, current: -0.3, target: 0, progress: 85 },
//     ...
//   ]
// }

Why State Tracking Matters

Instead of binary "fired/not fired" alerts, the state machine lets you see setups forming. When 5 out of 6 conditions are met, you can prepare. When the last condition fires, you're ready to act instantly. This is the difference between retail and institutional execution quality.

Building with market-alerts MCP

Creating Alerts Programmatically

create_alert
create_alert({
  name: "AAPL Breakout",
  symbol: "AAPL",
  conditions: "price crosses_above 195 AND volume > avg_volume * 1.5 AND rsi(14) > 55",
  actions: ["discord_notify:#trading-signals", "log_setup"],
  expiry: "2026-03-15",
  priority: "high",
  metadata: {
    strategy: "momentum_breakout",
    entry: 195.00,
    stop: 188.50,
    tp1: 205.00,
    tp2: 215.00,
    rr: 1.54,
    compositeScore: 88.5,
    grade: "A+"
  }
})

// Response:
{
  "id": "alert_7f8a9b2c",
  "status": "active",
  "created": "2026-03-05T23:15:00Z",
  "nextCheck": "2026-03-06T09:30:00Z"
}

Batch Alert Creation from Scanner

Post-Scanner Alert Pipeline
// After scanner identifies 10 A+ setups:
for (const setup of scannerResults) {
  create_alert({
    name: `${setup.ticker} ${setup.strategy}`,
    symbol: setup.ticker,
    conditions: generateConditions(setup),
    actions: [
      setup.grade === "A++" ? "discord_notify:#priority-alerts" : "discord_notify:#trading-signals",
      "log_setup"
    ],
    expiry: addDays(today, setup.horizon === "swing" ? 10 : 30),
    priority: setup.grade === "A++" ? "critical" : "high",
    metadata: {
      entry: setup.entry,
      stop: setup.stop,
      tp1: setup.tp1,
      tp2: setup.tp2,
      rr: setup.rr,
      compositeScore: setup.composite,
      grade: setup.grade,
      scanDate: today
    }
  });
}

function generateConditions(setup) {
  switch (setup.strategy) {
    case "oversold_bounce":
      return `rsi(14) crosses_above 30 AND price > ${setup.entry * 0.98}
              AND volume > avg_volume * 1.3`;
    case "momentum_breakout":
      return `price crosses_above ${setup.entry}
              AND volume > avg_volume * 1.5 AND rsi(14) > 55`;
    case "safe_haven":
      return `price > ema(20) AND vix > 25`;
    default:
      return `price within_range(${setup.entry * 0.98}, ${setup.entry * 1.02})
              AND volume > avg_volume`;
  }
}

Managing Alert Lifecycle

// List all active alerts
list_alerts({ status: "active" })

// Pause during earnings blackout
pause_alert({ id: "alert_7f8a9b2c", reason: "AAPL earnings tomorrow" })

// Snooze for 24 hours (e.g., FOMC day)
snooze_alert({ id: "alert_7f8a9b2c", duration: "24h" })

// Check specific alert status
get_alert_status({ id: "alert_7f8a9b2c" })
// → { state: "NEAR_CONFIRMED", met: 4, total: 5, lastCheck: "..." }

// Update conditions (e.g., tighten after partial move)
update_alert({
  id: "alert_7f8a9b2c",
  conditions: "price crosses_above 193 AND volume > avg_volume * 1.3"
})

// Delete expired or irrelevant alerts
delete_alert({ id: "alert_7f8a9b2c" })

Auto-Detection of Setup Confirmations

The killer feature: after the scanner identifies setups, the system automatically monitors for entry conditions and fires a CONFIRMED alert when all conditions align.

Complete Auto-Confirmation Flow
// 1. Scanner runs at 23:00 → identifies CF as A+ oversold bounce
// 2. Auto-creates monitoring alert:
create_alert({
  name: "CF Oversold Bounce — Scanner Mar 5",
  symbol: "CF",
  conditions: `
    rsi(14) crosses_above 30
    AND price > 76.50
    AND volume > avg_volume * 1.5
    AND macd_histogram > 0
  `,
  actions: ["discord_notify:#confirmed-setups", "create_trade_log"],
  expiry: "2026-03-15",
  priority: "critical",
  metadata: {
    entry: [76.50, 78.50],
    stop: 73.80,
    tp1: 83.00,
    tp2: 88.50,
    rr: 1.67,
    thesis: "Fertilizer stock oversold on broad market selloff. Strong fundamentals, insider buying, sector tailwinds from energy rally.",
    grade: "A+"
  }
})

// 3. System checks every 15 min during market hours
// 4. Day 2, 10:45 AM: RSI crosses above 30 ✓, volume 1.8x ✓
//    → State: NEAR_CONFIRMED (3/4 met, waiting for MACD)
// 5. Day 2, 14:30: MACD histogram turns positive ✓
//    → State: CONFIRMED! All 4 conditions met!
// 6. Discord notification fires:

// ═══════════════════════════════════════
// 🟢 CONFIRMED: CF Oversold Bounce
// ═══════════════════════════════════════
// Score: 86.2 | Grade: A+
// Strategy: Oversold Bounce | Regime: Early Risk-Off
//
// Entry Zone: $76.50 – $78.50
// Stop Loss:  $73.80 (-4.1%)
// Target 1:   $83.00 (+6.4%)
// Target 2:   $88.50 (+14.3%)
// R/R:        1.67
//
// Thesis: Fertilizer stock oversold on broad market selloff.
// Strong fundamentals, insider buying, sector tailwinds.
//
// Conditions Met:
// ✅ RSI(14) crossed above 30 (current: 32.1)
// ✅ Price > $76.50 (current: $77.85)
// ✅ Volume 1.8x average
// ✅ MACD histogram positive (0.12)
// ═══════════════════════════════════════

The Evolving DSL

The DSL grows over time based on feedback from retrospectives. Each version adds new condition types or refines existing ones.

VersionNew ConditionsTrigger
v1.0price, volume, rsi, macd, ema/smaInitial release
v1.1bb_squeeze, bb_width, atrSqueeze setups requested
v2.0earnings_in_days, pe_ratio, insider_buyingFundamental triggers needed
v2.1stocktwits_bull_ratio, reddit_mentionsSocial sentiment layer added
v2.2crosses_above, crosses_below operatorsState change detection
v3.0N_OF quantifier, sector_avg, vix conditionsAfter retro showed 15% hit rate improvement with sector filter
v3.1gap_up, gap_down, volume_profile_pocGap trading setups
v4.0composite_score, ai_conviction, regimeFull multi-layer integration
DSL Version Migration
// v2.0 alert (before sector filter):
"rsi(14) < 30 AND volume > avg * 2 AND price > sma(200)"

// v3.0 alert (after retro feedback — add sector + regime):
"rsi(14) < 30 AND volume > avg * 2 AND price > sma(200)
 AND sector_momentum > 0 AND regime IN (EARLY_RISK_OFF, RECOVERY)"

// Hit rate improvement: 42% → 57% (+15 percentage points)
// This is what the feedback loop (Part 6) enables.

DSL as Living Documentation

Every DSL version change is driven by real data from retrospectives. "We added sector_momentum because 60% of failed setups were in sectors with negative momentum." The DSL becomes a living record of what works and what doesn't.

Part 5 of 6
Discord Integration & Daily Pipeline
Automated Trading with Claude Code 4/6