TradeClaw

Open-source AI market intelligence for traders who prefer evidence over noise.

Self-hosted by default

Product

  • Dashboard
  • Screener
  • Backtest
  • Track record
  • Live demo

Transparency

  • What we tested and killed
  • Methodology
  • Why long-term
  • Open data
  • Calibration

Resources

  • Blog
  • Docs
  • API reference
  • How it works
  • FAQ
  • Glossary

Community

  • Discord
  • Weekly digest
  • Contribute
  • Contributors
  • Sponsors

Open source

  • GitHub repo
  • Star history
  • Self-host guide
  • Security
  • Data freshness
  • Roadmap

© 2026 TradeClaw. MIT licensed.

Terms|Privacy|Trading involves risk. Signals are informational only and are not financial advice.
DashboardScreenerCopilotTrack Record

How TradeClaw Works

Complete signal engine transparency — no black boxes

Open algorithm. No black boxes.

Every signal TradeClaw generates is produced by deterministic, open-source TypeScript code. This page documents exactly how it works — the data sources, indicator math, scoring formula, quality gates, and TP/SL calculation. If you disagree with any of it,submit a PR.

Signal Pipeline (5 steps)

Indicator Weights (max 90 points total)

RSI (14-period)
+20 pts
MACD (12/26/9)
+20 pts
EMA (20/50 alignment)
+20 pts
Bollinger Bands
+15 pts
Stochastic (14/3/3)
+15 pts
Score is mapped from 0–90 raw points to the existing 50–98 rule-score band. Minimum rule score: 55/100. This is mechanical indicator confluence, not a predictive probability.

Scoring Formula (exact code)

This is the actual function from apps/web/app/lib/signal-generator.ts. Simplified for clarity — see GitHub for full implementation.

typescript
// Mechanical signal rule scoring — apps/web/app/lib/signal-generator.ts

function scoreSignal(indicators: AllIndicators, direction: 'BUY' | 'SELL'): number {
  let score = 0;

  const { rsi, macd, ema, bollinger, stochastic } = indicators;
  const isBuy = direction === 'BUY';

  // ── RSI component (max 20 pts) ──────────────────────────
  if (isBuy) {
    if (rsi.current < 30) score += 20;       // Oversold: strong BUY signal
    else if (rsi.current < 40) score += 15;  // Approaching oversold
    else if (rsi.current < 50) score += 8;   // Below midline
    else if (rsi.current > 70) score -= 10;  // Overbought: penalise BUY
  } else {
    if (rsi.current > 70) score += 20;       // Overbought: strong SELL signal
    else if (rsi.current > 60) score += 15;
    else if (rsi.current > 50) score += 8;
    else if (rsi.current < 30) score -= 10;
  }

  // ── MACD component (max 20 pts) ─────────────────────────
  const { histogram, current: macdCurrent } = macd;
  const histLen = histogram.length;
  if (histLen >= 2) {
    const histChanging = isBuy
      ? histogram[histLen-1] > histogram[histLen-2]   // histogram increasing
      : histogram[histLen-1] < histogram[histLen-2];
    if (histChanging) score += 10;
  }
  if (isBuy && macdCurrent.macd > macdCurrent.signal) score += 10;
  if (!isBuy && macdCurrent.macd < macdCurrent.signal) score += 10;

  // ── EMA trend alignment (max 20 pts) ────────────────────
  const { current: emaCurrent } = ema;
  const price = indicators.closes[indicators.closes.length - 1];
  if (isBuy) {
    if (price > emaCurrent.ema20) score += 7;
    if (price > emaCurrent.ema50) score += 7;
    if (emaCurrent.ema20 > emaCurrent.ema50) score += 6;  // golden cross
  } else {
    if (price < emaCurrent.ema20) score += 7;
    if (price < emaCurrent.ema50) score += 7;
    if (emaCurrent.ema20 < emaCurrent.ema50) score += 6;  // death cross
  }

  // ── Bollinger Bands (max 15 pts) ────────────────────────
  const { current: bb } = bollinger;
  if (isBuy && price < bb.lower) score += 15;   // Price at lower band
  else if (isBuy && price < bb.middle) score += 8;
  if (!isBuy && price > bb.upper) score += 15;  // Price at upper band
  else if (!isBuy && price > bb.middle) score += 8;

  // ── Stochastic (max 15 pts) ──────────────────────────────
  const { current: stoch } = stochastic;
  if (isBuy && stoch.k < 20) score += 15;    // Stochastic oversold
  else if (isBuy && stoch.k < 40) score += 8;
  if (!isBuy && stoch.k > 80) score += 15;   // Stochastic overbought
  else if (!isBuy && stoch.k > 60) score += 8;

  // ── Normalise to 0–100 ──────────────────────────────────
  const maxScore = 90;
  const rawPct = Math.max(0, Math.min(100, (score / maxScore) * 100));

  // Preserve the existing 50-98 output band. This is not a probability.
  return 50 + rawPct * 0.48;
}

RSI Implementation (from scratch, no TA-Lib)

Wilder's original RSI formula, implemented in TypeScript. File: apps/web/app/lib/ta-engine.ts

typescript
// RSI calculation — apps/web/app/lib/ta-engine.ts

function calculateRSI(closes: number[], period = 14): RSIResult {
  const result: number[] = new Array(closes.length).fill(NaN);
  if (closes.length < period + 1) return { values: result, current: NaN };

  let gains = 0, losses = 0;

  // First RSI: simple average of first period
  for (let i = 1; i <= period; i++) {
    const delta = closes[i] - closes[i - 1];
    if (delta > 0) gains += delta;
    else losses -= delta;
  }

  let avgGain = gains / period;
  let avgLoss = losses / period;

  result[period] = avgLoss === 0 ? 100 : 100 - (100 / (1 + avgGain / avgLoss));

  // Wilder's smoothing for subsequent values
  for (let i = period + 1; i < closes.length; i++) {
    const delta = closes[i] - closes[i - 1];
    const gain = delta > 0 ? delta : 0;
    const loss = delta < 0 ? -delta : 0;
    avgGain = (avgGain * (period - 1) + gain) / period;
    avgLoss = (avgLoss * (period - 1) + loss) / period;
    result[i] = avgLoss === 0 ? 100 : 100 - (100 / (1 + avgGain / avgLoss));
  }

  return { values: result, current: result[result.length - 1] };
}

Take-Profit / Stop-Loss Logic

Levels derived from swing highs/lows in the last 20 candles, with ATR fallback.

typescript
// Take-profit / stop-loss calculation — signal-generator.ts

function calculateTPSL(
  direction: 'BUY' | 'SELL',
  entry: number,
  highs: number[],
  lows: number[],
  atr: number,
) {
  const recentHighs = highs.slice(-20).sort((a, b) => b - a);
  const recentLows = lows.slice(-20).sort((a, b) => a - b);

  const resistance = recentHighs[2];  // 3rd highest high = resistance
  const support = recentLows[2];      // 3rd lowest low = support

  if (direction === 'BUY') {
    // TP: nearest resistance above entry, or ATR-based fallback
    const tp1 = resistance > entry
      ? resistance
      : entry + atr * 1.5;
    // SL: nearest support below entry, or ATR-based fallback
    const sl = support < entry
      ? support
      : entry - atr * 1.0;
    return { tp1, sl, rr: (tp1 - entry) / (entry - sl) };
  } else {
    const tp1 = support < entry
      ? support
      : entry - atr * 1.5;
    const sl = resistance > entry
      ? resistance
      : entry + atr * 1.0;
    return { tp1, sl, rr: (entry - tp1) / (sl - entry) };
  }
}

Data Sources

Market-data hubConfigured primary

Primary candle source only when MARKET_DATA_HUB_URL is configured. The hub aggregates its own upstream providers.

Binance REST APICrypto fallback

Thin public-data fallback for supported crypto pairs when the hub is unavailable or has insufficient candles.

Stooq CSVTradFi fallback

Thin fallback for supported forex and metals symbols when the hub is unavailable or has insufficient candles.

Important disclaimer

TradeClaw signals are generated by algorithmic rules, not human analysis. Past signal accuracy does not guarantee future results. This tool is for educational and research purposes only. Always do your own research before trading.See accuracy page and calibration page for OHLCV-resolved signal-study results and their exclusions. These are not broker fills or portfolio performance.

Found a bug in the algorithm? Want to improve the scoring?

View source on GitHub