Programmable trading triggers with composable conditions, automatic setup confirmation, state machine tracking, and the market-alerts MCP server.
Hardcoding alert conditions in scripts means every change requires code edits, testing, and deployment. A DSL gives you:
rsi(14) crosses_above 30 AND volume > avg * 2 is self-documentingThe market-alerts MCP server provides 7 tools: create_alert, list_alerts, update_alert, delete_alert, pause_alert, snooze_alert, get_alert_status.
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 > avg_volume * 2.0
volume_surge > 3x
relative_volume > 2.5
volume_trend == rising(5d)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.5earnings_in_days <= 5
pe_ratio < sector_avg * 0.8
insider_buying_90d > 3
revenue_growth_yoy > 20
short_interest > 20%stocktwits_bull_ratio > 0.7
reddit_mentions_24h > 100
news_sentiment == positive
social_volume_spike > 3xcondition_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 4within_market_hours
premarket
after_hours
day_of_week IN (Mon, Tue, Wed)
not_opex_weekalert := 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 := '"' [^"]* '"'
Pre-built alert patterns for common trading setups. Each template is a tested combination of conditions that has been validated through backtesting.
price crosses_above resistance(1)
AND volume > avg_volume * 1.5
AND rsi(14) > 50
AND rsi(14) < 75
AND within_market_hoursrsi(14) crosses_above 30
AND price > support(1)
AND macd crosses_above signal
AND volume > avg_volume * 1.2earnings_in_days <= 2
AND iv_rank > 70
AND put_call_ratio < 0.7
AND NOT bb_squeezebb_width < percentile(10)
AND atr(14) < sma(atr(14), 20) * 0.7
AND volume_trend == contracting(5d)
AND price > sma(200)price < sma(200)
AND rsi(14) < 25
AND insider_buying_90d > 0
AND pe_ratio < sector_avg * 0.7price > ema(20)
AND ema(20) > ema(50)
AND ema(50) > ema(200)
AND volume_trend == rising(5d)
AND rsi(14) > 55 AND rsi(14) < 72vix crosses_above 25
OR (spy_rsi(14) < 30 AND vix > 20)
OR (dxy_change_5d > 2 AND yield_10y_change_5d > 0.15)insider_buying_90d >= 3
AND insider_buy_value > 500000
AND price < sma(200)
AND short_interest < 10%Alerts don't just fire once. They track a setup through multiple states from initial detection to confirmation to execution.
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 },
// ...
// ]
// }
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.
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"
}
// 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`;
}
}
// 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" })
The killer feature: after the scanner identifies setups, the system automatically monitors for entry conditions and fires a CONFIRMED alert when all conditions align.
// 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 DSL grows over time based on feedback from retrospectives. Each version adds new condition types or refines existing ones.
| Version | New Conditions | Trigger |
|---|---|---|
| v1.0 | price, volume, rsi, macd, ema/sma | Initial release |
| v1.1 | bb_squeeze, bb_width, atr | Squeeze setups requested |
| v2.0 | earnings_in_days, pe_ratio, insider_buying | Fundamental triggers needed |
| v2.1 | stocktwits_bull_ratio, reddit_mentions | Social sentiment layer added |
| v2.2 | crosses_above, crosses_below operators | State change detection |
| v3.0 | N_OF quantifier, sector_avg, vix conditions | After retro showed 15% hit rate improvement with sector filter |
| v3.1 | gap_up, gap_down, volume_profile_poc | Gap trading setups |
| v4.0 | composite_score, ai_conviction, regime | Full multi-layer integration |
// 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.
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.