DMTrading Rules API

Write alerting rules as JSON, submit them with an API key, and we run them against exchange and on-chain market data and tell you when they fire — with a stamped record of what happened next.

You are probably here to hand this page to your own assistant. That is the intended way to use it. The machine-readable contract is /client/api/openapi.json; the DSL vocabulary and JSON Schema are at /client/api/vocabulary; and there is a copy-pasteable prompt at the bottom of this page.

A rule reports that a measurement crossed a line, and reports its own record of what happened at +1h and +24h. Nothing here is advice or a forecast, and there is no order endpoint — this API cannot buy or sell anything.

Assistant workflow, market reads and baskets

Start with get_capabilities. Resolve markets, preflight the rule, create when authorized, then read back its status. Valid JSON is not a ready alert. Candle-read intervals are separate from signal evaluation timeframes.

basket.v1 applies one setup/timeframe to a fixed symbols list or a top-N spot volume universe. Each active or paused member uses one signal slot. Top-N membership refreshes after catalog sync; removed members retain their history. One-shot applies per member. Use the returned member IDs to read individual fire histories.

GET /client/api/capabilities · get_capabilities
Discover rule families, offered venues and catalog freshness, separate read/evaluation timeframes, account quota and delivery readiness. Start here; offered does not mean ready. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/markets · list_markets
Find spot symbols. volume_24h_usd returns the curated top-20 USD-equivalent volume ranking (not order-book liquidity). sort=symbol searches all online instruments. Missing/stale catalogs are explicit. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/ohlcv · get_ohlcv
Read up to 300 recent spot OHLCV bars from the exchange/chart cache, with decimal strings, closed/forming flags, gaps and freshness. Read intervals do not imply signal evaluation support. Upstream failures are unknown, never zero. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/ohlcv/batch · get_ohlcv_batch
Read up to ten spot markets in one bounded call. source=market returns exchange chart bars; source=engine returns the stored successor-confirmed rollups used by signal evaluation. Each symbol has its own success, invalid or unavailable result. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/derivatives/series · get_derivatives_series
Read up to 300 stored public Bybit derivatives observations aligned to a requested grain. Values are exact decimal strings; missing and stale inputs carry blockers and never become zero. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/derivatives/snapshot · get_derivatives_snapshot
Read a bounded public Bybit funding, open-interest, USD open-interest and perp/spot-basis snapshot for up to 20 explicitly named spot symbols. Stale fields are null with blockers. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/levels · get_market_levels
Calculate exact prior-range, confirmed swing and rolling VWAP levels from the engine's successor-confirmed closed bars. The response pins the calculation version, UTC session, lookback inclusion policy and missing-input blockers. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/assistant/context · get_assistant_context
Read one bounded snapshot containing quota, safe delivery readiness, a selected venue's top markets, owned rules and baskets, active preparation blockers, and recent fires. It contains no channel destinations or key secrets. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/onchain/assets/{id} · get_onchain_asset
Read one on-chain asset, its canonical pool, freshness and the exact current/previous watcher inputs plus successor-confirmed hourly candles. Missing fields remain null and are listed as unknown. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/markets/prepare · prepare_market
Queue a bounded, deduplicated history preparation for one listed market, setup and signal timeframe. Poll the returned preparation until ready or blocked; provider failures remain explicit and retryable. Required scopes: rules:write, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/market-preparations/{id} · get_market_preparation
Read one owned market-preparation request with requested depth, stored coverage, gaps, the latest engine-confirmed bar, progress and a retryable blocker. Required scopes: market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/preflight · preflight_rule
Check a watcher.v1 or signal.v1 rule plus delivery settings without saving. Returns valid and ready separately, canonical rule, quota, data, delivery and halt blockers. Readiness is a snapshot, not a reservation. Requires rules:read and market:read. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/price-alerts · create_price_alert
Create an observed-tick exact Decimal price crossing or percent-move alert on one listed spot market. A crossing uses persisted before/after observations; missing or stale prices are reported as unknown. Use condition.v1 close_above, close_below, enter or leave for successor-confirmed closed-bar alerts. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/price-alerts · list_price_alerts
List owned price alerts with exact thresholds, revisions and evaluation health. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/price-alerts/{id} · get_price_alert
Read one owned price alert. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
PATCH /client/api/price-alerts/{id} · update_price_alert
Patch one owned price alert. expected_revision is required so concurrent logic or routing edits cannot overwrite each other. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/price-alerts/{id}/pause · pause_price_alert
Pause evaluation of one owned price alert. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/price-alerts/{id}/resume · resume_price_alert
Resume one owned price alert; the next fresh observation seeds a new crossing baseline. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
DELETE /client/api/price-alerts/{id} · delete_price_alert
Delete one owned price alert and its fire history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/price-alerts/{id}/fires · get_price_alert_fires
Read bounded fire history with exact observed prices and correlated delivery state. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/condition-alerts/preflight · preflight_condition_alert
Validate one condition.v1 document and report closed-bar data, delivery, quota and halt blockers without saving. It supports exact close_above/close_below, inclusive enter/leave zones and persisted sweep_reclaim sequences. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/condition-alerts · create_condition_alert
Save a condition.v1 document. A ready document becomes active; a valid blocked document is saved explicitly as a draft with its blockers. Required scopes: rules:write, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/condition-alerts · list_condition_alerts
List owned advanced conditions with revisions, persisted sequence state and evaluation health. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/condition-alerts/{id} · get_condition_alert
Read one owned advanced condition. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
PATCH /client/api/condition-alerts/{id} · update_condition_alert
Replace one condition document and settings with an expected revision. Logic changes create a new immutable revision and reset sequence state. Required scopes: rules:write, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/condition-alerts/{id}/pause · pause_condition_alert
Pause closed-bar evaluation while preserving state and history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/condition-alerts/{id}/resume · resume_condition_alert
Recheck readiness and resume; unresolved blockers leave an explicit draft. Required scopes: rules:write, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
DELETE /client/api/condition-alerts/{id} · delete_condition_alert
Delete one owned advanced condition and its version/fire history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/condition-alerts/{id}/fires · get_condition_alert_fires
Read observed fires with exact price, closed-bar trace and correlated delivery state. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/condition-alerts/{id}/versions · get_condition_alert_versions
Read immutable condition logic revisions. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/condition-alerts/replay · replay_condition_alert
Evaluate condition.v1 with the live closed-bar engine and return bounded historical example fires without arming or mixing them into observed statistics. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/signal-baskets/preflight · preflight_signal_basket
Resolve and check a basket.v1 setup across fixed symbols or dynamic top_n volume markets. Reports the separate basket and active-member evaluation budgets before creation; saves nothing. Requires rules:read and market:read. Required scopes: rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/signal-baskets · create_signal_basket
Create a basket.v1 atomically after preflight. It uses one basket allowance plus one active-member evaluation unit per selected market; standalone signal slots are separate. top_n refreshes on successful catalog sync; symbols is fixed. one_shot applies per member. Requires rules:write, rules:read and market:read. Required scopes: rules:write, rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/signal-baskets · list_signal_baskets
List your signal baskets, desired membership, member rule IDs, readiness and retained history. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
GET /client/api/signal-baskets/{id} · get_signal_basket
Read one owned basket and its members. Member fire histories use get_signal_alert_fires with the returned rule IDs. Required scopes: rules:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
PATCH /client/api/signal-baskets/{id} · update_signal_basket
Patch one or more fields on an owned basket after preflight. expected_revision is required; stale edits return the current revision. Omitted fields retain their stored values, plan:null clears a plan, and a complete definition remains valid. Membership and delivery changes activate atomically and child alerts cannot be edited independently. Required scopes: rules:write, rules:read, market:read. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/signal-baskets/{id}/pause · pause_signal_basket
Pause a basket and its active members; retain membership, quota and history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
POST /client/api/signal-baskets/{id}/resume · resume_signal_basket
Resume a basket, retry membership readiness and preserve per-market one-shot/hysteresis state. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.
DELETE /client/api/signal-baskets/{id} · delete_signal_basket
Delete your basket, its member alerts and their fire history. Required scopes: rules:write. Read the returned status and reasons before reporting success; saved rules, available data and successful delivery are separate facts.

Two kinds of rule

There are two document classes, and which one you want follows from what you are watching.

The two version independently, because the version string pins a table and the two classes have nothing in their tables in common. Everything else is shared: the same key, the same rules:read / rules:write scopes, the same response envelope, the same structured errors and the same tier limits. Learn this surface once and you can write either.

Getting a key

Sign in with your Solana wallet, open API keys, and create one. The plaintext is shown exactly once and only its hash is stored, so if you lose it, rotate rather than recover. Send it as a bearer token:

curl -H "Authorization: Bearer dmt_live_…" \
  https://app.dutchmantrading.com/client/api/vocabulary

A key carries explicit scopes, and an endpoint that needs one it lacks answers 403 naming the scope:

Scope Grants
rules:read Read your rules, their fires, and their stamped track records
rules:write Create, update, pause and delete your rules
market:read Read asset, pool, liquidity and candle data

How much you can run is sized by the NSF you hold in that wallet — nothing is spent or deducted. /client/api/usage reports the tier, the limits and how much of them is in use.

Endpoints

Method Path Scope What it does
GET /client/api/vocabulary The field table, grammar, limits and JSON Schema.
POST /client/api/rules/validate rules:read Check a document. Stores nothing; returns every error at once.
GET /client/api/rules rules:read Your rules, newest first, each with its summary and track record.
POST /client/api/rules rules:write Store a rule and start evaluating it.
GET /client/api/rules/{id} rules:read One rule.
PATCH /client/api/rules/{id} rules:write Change a rule. The document is re-validated only when it changes.
DELETE /client/api/rules/{id} rules:write Delete a rule and its fire history. Permanent.
POST /client/api/rules/{id}/pause rules:write Stop evaluating it. Still occupies a tier slot.
POST /client/api/rules/{id}/resume rules:write Evaluate it again and clear the gate reason.
GET /client/api/rules/{id}/fires rules:read What it reported, with the +1h / +24h outcomes stamped on each.
GET /client/api/signal-alerts/schema market:read The setup catalog: every setup, its timeframes, its warm-up and what it means.
GET /client/api/signal-alerts/availability market:read Whether a market can carry a setup today, and where it cannot, why.
POST /client/api/signal-alerts/validate rules:read Check a signal-alert document. Stores nothing.
GET /client/api/signal-alerts rules:read Your signal alerts, newest first.
POST /client/api/signal-alerts rules:write Store a signal alert and start evaluating it.
GET /client/api/signal-alerts/{id} rules:read One signal alert.
PATCH /client/api/signal-alerts/{id} rules:write Change one. A partial document is merged onto the stored one and re-checked.
DELETE /client/api/signal-alerts/{id} rules:write Delete it and its fire history. Permanent.
POST /client/api/signal-alerts/{id}/pause rules:write Stop evaluating it. Still occupies a tier slot.
POST /client/api/signal-alerts/{id}/resume rules:write Evaluate it again, re-armed, and clear the gate reason.
GET /client/api/signal-alerts/{id}/fires rules:read What it reported, with the +1h / +24h outcomes stamped on each.
GET /client/api/usage rules:read Tier, limits, what is in use, and this key's counters.
POST /client/api/mcp per tool Model Context Protocol endpoint carrying the same operations.

The coin-watcher document

Every rule declares the vocabulary version it was written against — currently watcher.v1. That string is a contract: a stored rule keeps meaning what it meant when it was accepted, so the field table changing is a new version rather than an edit in place.

A document has up to five keys. universe chooses which coins are considered at all; when is the condition tested against each of them. name and note are the only free text, capped at 80 and 500 characters and scrubbed of control and invisible characters before storage.

POST /client/api/rules
Authorization: Bearer dmt_live_…
Content-Type: application/json

{
  "rule": {
    "dsl_version": "watcher.v1",
    "name": "Solana trenchers",
    "note": "the 100k / 5 day screen",
    "universe": {"chain": "solana", "gate_passed": true},
    "when": {"all": [
      {"above": {"left": "liquidity_usd", "right": {"value": 100, "unit": "k"}}},
      {"below": {"left": "pool_age_hours", "right": {"value": 5, "unit": "days"}}},
      {"above": {"left": "volume_24h_usd", "right": {"value": 250, "unit": "k"}}},
      {"cross_above": {"left": "price_change_1h_pct", "right": 15}}
    ]}
  },
  "once_per_asset": true,
  "cooldown_seconds": 3600
}

"Tell me when a Solana coin that has cleared the gate holds $100k liquidity inside its first five days, is actually trading, and jumps 15% in an hour." Every field in it is one this deployment computes right now, so this is the example to copy. Thresholds take a unit object, so 5 days is exact rather than a hand-computed 120.

cross_above compares this bar against the previous one, so the last condition fires on the bar the move crosses 15% rather than on every bar it stays above it. Cross operators are only offered on fields the engine carries a previous-bar value for — the table below says which.

The coin-watcher vocabulary

Closed and generated from the validator, so this table is what is actually enforced. A field that is not here is an error, never a silently dropped condition. universe fields select the coin set; condition fields are measured per coin and compared inside when.

Field Scope Type Unit Operators Range Available What it measures
chain universe enum is, one_of base, robinhood, solana yes Which chain the coin trades on. Give one chain as a string or several as an array. Naming a chain is the single largest reduction in a rule's evaluation cost; omitting it scans every chain.
trending universe boolean is true, false yes True while the coin appears on a data aggregator's trending list. This is a statement about that aggregator's output, computed by methods it does not publish, not an independent measurement of the market.
gate_passed universe boolean is true, false yes True once the coin has cleared the mechanical liquidity, age and staleness gate. A coin we have not checked yet reads as false, never as true.
liquidity_usd condition number usd (b, k, m, usd) above, below, between 0 to 1,000,000,000,000 yes US-dollar reserve held in the coin's canonical pool. Has no previous-bar value, so it takes no cross operator; pair "above" with once_per_asset to fire the first time a coin reaches a level.
pool_age_hours condition number hours (days, hours, minutes, weeks) above, below, between, cross_above 0 to 87,600 yes Hours since the pool was created, falling back to when we first saw the coin. Accepts a unit object such as {"value": 5, "unit": "days"}. Age only increases, so cross_above fires once when a pool comes of age and cross_below can never be true.
volume_24h_usd condition number usd (b, k, m, usd) above, below, between 0 to 1,000,000,000,000 yes US-dollar volume traded through the canonical pool over the trailing 24 hours, as reported by the pool. No previous-bar value, so no cross operator.
price_usd condition number usd (b, k, m, usd) above, below, between 0 to 1,000,000,000 yes Latest US-dollar price from the canonical pool, falling back to the last confirmed hourly close. No previous-bar value, so no cross operator; use a price_change field to talk about movement.
price_change_1h_pct condition number percent above, below, between, cross_above, cross_below -100 to 10,000,000 yes Percentage change in close price over the last hour. Negative for a fall; cannot go below -100.
price_change_6h_pct condition number percent above, below, between, cross_above, cross_below -100 to 10,000,000 yes Percentage change in close price over the last six hours. Negative for a fall; cannot go below -100.
price_change_24h_pct condition number percent above, below, between, cross_above, cross_below -100 to 10,000,000 yes Percentage change in close price over the last 24 hours. Negative for a fall; cannot go below -100.
attention_mentions condition integer count above, below, between, cross_above, cross_below 0 to 100,000,000 no Raw count of posts mentioning the coin on X in the most recent complete hourly sample. A raised count means people are posting; coordinated posting is indistinguishable from organic posting at this resolution.
Not available: No X credential is configured on this deployment, so no attention samples are being collected and this field is nil for every coin. A rule using it is accepted and stored, and holds on every pass until the source is connected — at which point it starts firing with no change to this contract and no new dsl_version.
attention_z condition number sigma above, below, between, cross_above, cross_below -1,000 to 1,000 no Standard deviations the latest mention count sits above the coin's own 24-hour baseline, so a quiet ticker and a loud one are measured on the same scale. Needs a baseline: a coin sampled for the first time has no z-score.
Not available: No X credential is configured on this deployment, so no attention samples are being collected and this field is nil for every coin. A rule using it is accepted and stored, and holds on every pass until the source is connected — at which point it starts firing with no change to this contract and no new dsl_version.
attention_ratio condition number ratio (ratio, x) above, below, between, cross_above, cross_below 0 to 100,000 no Latest mention count divided by the coin's own 24-hour baseline mean, so 3 means three times its usual chatter. Needs the same baseline as attention_z.
Not available: No X credential is configured on this deployment, so no attention samples are being collected and this field is nil for every coin. A rule using it is accepted and stored, and holds on every pass until the source is connected — at which point it starts firing with no change to this contract and no new dsl_version.

above and below are strict; between is inclusive on both bounds. A rule may nest 6 levels deep, hold 32 comparisons, and put 16 nodes in a single group.

Published is not the same as available

The Available column is about this deployment, not about the DSL version. A field marked no is part of watcher.v1 and is accepted by the validator — but nothing is producing a value for it, so a rule that uses it stores and then holds on every pass instead of firing. The flag is read when this page is rendered, so it changes on its own when a source is connected, without a new version and without a rule being rewritten. The same flag is on every field of /client/api/vocabulary, where a machine should read it.

A second example: a mention spike, once the attention source is connected

This is the request people actually ask for — "tell me when it gets 100 mentions on X" — and it is the reason the attention fields are in the vocabulary. It needs the attention source, which is not connected on this deployment yet, so read the Available column above before you copy this one.

{
  "dsl_version": "watcher.v1",
  "name": "Mention spike on a listed coin",
  "universe": {"chain": ["base", "solana"], "trending": true},
  "when": {"all": [
    {"above": {"left": "attention_mentions", "right": 100}},
    {"above": {"left": "liquidity_usd", "right": {"value": 50, "unit": "k"}}}
  ]}
}

Note the two shapes. The first example wraps the document under rule so it can also set delivery settings; this one is the bare document. /rules and /rules/validate accept either, because a document is what the vocabulary's own skeleton looks like and the obvious first attempt should not fail on a technicality.

Signal alerts

The other rule class. A signal alert declares signal.v1 and is four required values — the setup, the venue, the market and the timeframe — plus optional params and an optional note, capped at 280 characters. For this class those four values are the rule, which is why they sit in the document rather than beside it. Delivery settings — channel_ids, cooldown_seconds, one_shot — stay outside it, exactly as they do for a watcher.

POST /client/api/signal-alerts
Authorization: Bearer dmt_live_…
Content-Type: application/json

{
  "dsl_version": "signal.v1",
  "setup": "rsi_oversold",
  "exchange": "coinbase",
  "symbol": "BTC-USD",
  "timeframe": "h1",
  "note": "the hourly dip screen"
}

There is no name to set, and no name column behind one. The name in every response is generated from the rule itself — "RSI crosses into oversold on the 1 day — SOL-USD (Coinbase)" — so a label can never disagree with the thing it labels. note is your own text.

A symbol is only meaningful next to its exchange: the same pair is SOL-USD on one venue and SOLUSDT on another. Matching ignores case, and what gets stored is the venue's own spelling — so diff the canonical document that comes back from /client/api/signal-alerts/validate against what you sent. Venues: Coinbase, Binance.US, Bybit.

The setups

Curated and closed. Each one maps to a single classical condition that can be explained in a sentence, and a structure that is not in this table has no representation to reach. Buy watch and Sell watch name the side a structure signals, because a structure signals one side and offering the other would teach a false reading. Neither is an instruction to trade.

Timeframes are per setup and the narrower constraint: a golden cross is defined on daily and weekly closes, and on an hourly candle the same arithmetic is noise rather than the thing traders mean by the term. Warm-up is closed bars on the alert's own timeframe. A setup will not evaluate until the market has that many; on a weekly timeframe that is years of history.

Setup Side Timeframes Warm-up What it means
ltf_hf_buy
LTF breakout with volume (buy watch)
Buy watch m15 20 bars

Version 1 fires when a confirmed, closed 15-minute candle closes strictly above the highest high of the preceding 20 consecutive candles, and its base-asset volume is at least 1.5 times their average volume. The current candle is excluded from both baselines; missing bars or unavailable volume block evaluation. After firing, it re-arms only when a later close returns inside or onto that side of the rolling 20-bar range. Cooldown and one-shot settings still apply. This is a documented range-break approximation, not a reproduction of a TradingView HF indicator, an intrabar high-frequency strategy, or a recommendation to trade.

ltf_hf_sell
LTF breakdown with volume (sell watch)
Sell watch m15 20 bars

Version 1 fires when a confirmed, closed 15-minute candle closes strictly below the lowest low of the preceding 20 consecutive candles, and its base-asset volume is at least 1.5 times their average volume. The current candle is excluded from both baselines; missing bars or unavailable volume block evaluation. After firing, it re-arms only when a later close returns inside or onto that side of the rolling 20-bar range. Cooldown and one-shot settings still apply. This is a documented range-break approximation, not a reproduction of a TradingView HF indicator, an intrabar high-frequency strategy, or a recommendation to trade.

rsi_oversold
RSI crosses into oversold
Buy watch m15, h1, h4, h12, d1, w1 50 bars

RSI (Relative Strength Index) runs 0–100 and measures how one-sided recent moves have been. Below 30 is traditionally called "oversold" — selling has dominated. Traders watch it as a place where a bounce *may* form, but a market can stay oversold for a long time in a strong downtrend.

rsi_recovery
RSI recovers out of oversold
Buy watch m15, h1, h4, h12, d1, w1 50 bars

This fires when RSI climbs back *above* 30 after being oversold — the momentum downturn easing rather than the dip itself. Many traders consider the turn back up a cleaner reference point than the moment price first became oversold.

rsi_overbought
RSI crosses into overbought
Sell watch m15, h1, h4, h12, d1, w1 50 bars

Above 70, RSI is traditionally "overbought" — buying has dominated recently. It's watched as a spot where upward momentum *may* be stretched, but like oversold it can persist through a strong uptrend, so it is a caution flag, not a top.

golden_cross
Golden cross (50 over 200)
Buy watch d1, w1 260 bars

A "golden cross" is the 50-period moving average crossing above the 200-period one — a slow, trend-following signal that the medium-term average has overtaken the long-term average. It is defined on daily and weekly closes; on intraday candles the same math is dominated by noise and isn't what traders mean by the term.

death_cross
Death cross (50 under 200)
Sell watch d1, w1 260 bars

The mirror of the golden cross: the 50-period average crossing *below* the 200-period one, read as the trend turning down over a medium horizon. Also a daily/weekly signal — it lags by design, describing a shift that has already begun.

price_reclaims_ma
Price reclaims a moving average
Buy watch h4, h12, d1, w1 80 bars (period 50); 230 bars (period 200)

Fires when price closes back above its moving average after being below it. The moving average is a common reference for the prevailing trend, so reclaiming it is watched as short-term strength. Choose the 50 for a medium reference or the 200 for the long-term one.

Params: period — one of 50, 200, default 50
price_loses_ma
Price loses a moving average
Sell watch h4, h12, d1, w1 80 bars (period 50); 230 bars (period 200)

Fires when price closes below its moving average after being above it — the mirror of reclaiming it, watched as short-term weakness relative to the trend reference. The 50 tracks a medium horizon, the 200 the long-term one.

Params: period — one of 50, 200, default 50
bollinger_lower
Price pierces the lower Bollinger band
Buy watch h1, h4, h12, d1 40 bars

Bollinger Bands sit two standard deviations either side of a 20-period average, so they widen and narrow with volatility. A close below the lower band means price is unusually far below its recent average — a stretch that sometimes mean-reverts, though in a strong down-move price can "walk the band" lower.

bollinger_upper
Price pierces the upper Bollinger band
Sell watch h1, h4, h12, d1 40 bars

A close above the upper Bollinger band means price is unusually far above its recent 20-period average — an extension that can mean-revert, but which also occurs during strong breakouts, so it reads as "stretched," not "reversing."

macd_bullish
MACD bullish signal cross
Buy watch h4, h12, d1, w1 80 bars

MACD tracks the gap between a fast and a slow moving average, and a "signal line" smooths that gap. The MACD line crossing above the signal line is a classic momentum-turning-up marker. It whipsaws on short timeframes, so it's offered on 4h and above.

macd_bearish
MACD bearish signal cross
Sell watch h4, h12, d1, w1 80 bars

The MACD line crossing *below* its signal line — the mirror of the bullish cross, read as momentum turning down. Also offered on 4h and above, where the signal is less prone to whipsaw.

msb_bullish
Bullish structure break (new higher high)
Buy watch m15, h1, h4, h12, d1, w1 60 bars

Markets that are falling make a series of lower highs. When price finally closes above the most recent one, that sequence is broken — traders call it a bullish market-structure break (MSB, or BOS). It is the moment a downtrend stops making lower highs, which is why it sits under Buy watch: it is the condition someone looking for a long waits for.

The level itself is a "swing high" — a bar standing above the two bars on each side of it. Needing bars on BOTH sides is what makes it a pivot rather than just a recent high, and it is also why it can only be confirmed two bars after it printed. This alert fires on a break of a confirmed level, never a guessed one.

It marks the end of the old structure, not the start of a reliable new one. Plenty of structure breaks fail inside an ongoing downtrend, and a condition being met is information, not advice.

msb_bearish
Bearish structure break (new lower low)
Sell watch m15, h1, h4, h12, d1, w1 60 bars

Markets that are rising make a series of higher lows. When price closes below the most recent one, that sequence is broken — the mirror of the bullish case, and the moment an uptrend stops making higher lows. It sits under Sell watch because it is the condition someone looking to exit a long, or to short, watches for.

The level is a "swing low": a bar sitting below the two bars on each side of it. Because it needs bars on both sides to be a pivot, it is confirmed two bars after it printed, and this fires only on a break of a confirmed level.

As with the bullish case, it marks the end of the previous structure rather than the start of a dependable new one. A condition being met is information, not advice.

Only the two moving-average setups take a param, and it is the one choice the catalog leaves open: the 50 for a medium trend reference or the 200 for the long-term one. Asking for anything else is an error rather than a silent correction, so you learn what the setup offers instead of being quietly given the default. This example also shows the wrapped request shape, which is how you set delivery settings — both shapes are accepted here and on /client/api/rules.

{
  "rule": {
    "dsl_version": "signal.v1",
    "setup": "price_reclaims_ma",
    "exchange": "coinbase",
    "symbol": "SOL-USD",
    "timeframe": "d1",
    "params": {"period": 200}
  },
  "cooldown_seconds": 86400,
  "one_shot": false
}

Whether a market can carry a setup is a separate question

The table above is the whole static contract, and it is deliberately silent about availability. Whether golden_cross can run on a given market today depends on how much history and how much traded volume that market has — facts about one market at one moment, which a table published here would be wrong about within the day.

Ask /client/api/signal-alerts/availability?exchange=…&symbol=… instead. It answers per setup and per timeframe, and where the answer is no it says why in a code you can branch on, with the numbers behind it and the sentence a person would be shown:

GET /client/api/signal-alerts/availability?exchange=coinbase&symbol=SOL-USD&setup=golden_cross

{
  "data": {
    "exchange": "coinbase",
    "exchange_label": "Coinbase",
    "symbol": "SOL-USD",
    "setups": [
      {
        "id": "golden_cross",
        "label": "Golden cross (50 over 200)",
        "side": "buy_watch",
        "timeframes": [
          {"id": "d1", "label": "1 day", "available": true,
           "reason": null, "short": null, "detail": null},
          {"id": "w1", "label": "1 week", "available": false,
           "reason": "insufficient_history",
           "bars_held": 214, "bars_needed": 260,
           "short": "Not enough history",
           "detail": "This market has 214 weekly closes; the setup needs 260."}
        ]
      }
    ]
  }
}

This matters most on the long timeframes. A weekly moving-average cross warms up on 260 weekly closes — five years of history — and without asking first, the only way to discover that is to arm the alert and wait. That endpoint is the one call here that loads a market's candle history, so it is metered per key; over the limit is 429 with a retry-after header, which is a throttle and not an authorization failure.

Error codes for this class: empty, invalid_type, invalid_value, missing_key, not_an_object, out_of_range, too_long, too_many_keys, unknown_key, unknown_market, unsupported_timeframe, unknown_plan_ref, invalid_plan_target, invalid_plan_offset, plan_too_many_targets, wrong_dsl_version. Two of them are its own — unknown_market when the venue does not list the symbol, and unsupported_timeframe when the setup is real but is not defined on the timeframe you asked for.

When a rule is rejected

The validator returns every problem in the document at once, not the first one, each with a path into the document and usually a concrete correction. Correcting one error per round trip is slow, and worse, a fix for an early error can quietly create a later one.

HTTP/1.1 422 Unprocessable Content

{
  "error": {
    "code": "invalid_rule",
    "dsl_version": "watcher.v1",
    "message": "The rule was not accepted, and nothing was saved. 2 problems were found; every one of them is listed in \"errors\". Fix them all and resubmit.",
    "errors": [
      {
        "path": "when.all[0].above.left",
        "code": "unknown_field",
        "message": "\"liqidity_usd\" is not a field this engine computes, so the condition was not applied.",
        "expected": ["liquidity_usd", "pool_age_hours", "volume_24h_usd", "…"],
        "got": "liqidity_usd",
        "suggestion": "did you mean \"liquidity_usd\"?"
      },
      {
        "path": "when.all[1].cross_above",
        "code": "unsupported_operator",
        "message": "\"cross_above\" is not valid on \"liquidity_usd\".",
        "expected": ["above", "below", "between"],
        "got": "cross_above",
        "suggestion": "\"liquidity_usd\" has no previous-bar value, so a cross could never be true — use \"above\" with once_per_asset to fire on the first crossing"
      }
    ]
  }
}

Codes are a closed set: ambiguous_node, empty, invalid_range, invalid_type, invalid_unit, invalid_value, missing_key, not_an_object, out_of_range, too_deep, too_expensive, too_long, too_many_conditions, too_many_keys, too_many_nodes, unknown_field, unknown_key, unknown_operator, unsupported_operator, wrong_dsl_version, wrong_scope.

MCP

The same operations are available over Model Context Protocol at /client/api/mcp — JSON-RPC 2.0 over HTTP POST, with the key as a bearer token. Both rule classes are there, and a tool's name says which one it speaks for.

Coin watchers: get_vocabulary, validate_rule, create_rule, list_rules, get_rule, pause_rule, resume_rule, delete_rule, get_fires.

Signal alerts: get_signal_setups, check_signal_availability, validate_signal_alert, create_signal_alert, list_signal_alerts, get_signal_alert, update_signal_alert, pause_signal_alert, resume_signal_alert, delete_signal_alert, get_signal_alert_fires.

And get_usage, for the account both classes are counted against.

Scopes apply per tool, not per connection: a rules:read key sees the read tools of both classes and is refused every write. tools/list returns only what your key can call. The two catalog tools — get_vocabulary and get_signal_setups — need no scope beyond a valid key, because they publish what the engine speaks and say nothing about any account.

check_signal_availability needs market:read and spends the same per-key allowance as GET /client/api/signal-alerts/availability. It is the same candle load either way, so it is one budget rather than one per protocol.

A prompt you can hand your own AI

Paste this, with your key in place of the placeholder.

You can use the DMTrading rules API at https://app.dutchmantrading.com/client/api.
Use my API key as `Authorization: Bearer <key>`; never repeat it in replies.
Start with GET /client/api/capabilities (get_capabilities over MCP).
Resolve symbols with /markets; use /ohlcv for chart analysis, without a rule.
Preflight rules at /preflight; a valid document may still have readiness blockers.
For multiple exchange markets use basket.v1 at /signal-baskets, with
/signal-baskets/preflight first. Fixed symbols or top-N volume membership;
one signal slot per active/paused member. Read back status after creation.
(If you speak MCP, connect to https://app.dutchmantrading.com/client/api/mcp
with that key as a bearer token instead, and use the tools it offers. Both
kinds of rule below are there: the *_rule tools are coin watchers and the
*_signal_alert tools are signal alerts, and every step below has a tool with
the obvious name — get_vocabulary, get_signal_setups,
check_signal_availability, validate_rule, validate_signal_alert.)

There are two kinds of rule. Decide which one I am asking for before you write
anything:

  * A COIN WATCHER, at /client/api/rules, when I have not named a coin — "any
    Solana coin that does X". It finds the coins itself.
  * A SIGNAL ALERT, at /client/api/signal-alerts, when I name a market on an
    exchange and a classical technical setup — "tell me when SOL-USD goes
    oversold on the daily". The setups are a fixed list, not a formula.

If I asked for a named market, do not build a watcher for it, and if I asked
for a screen across coins, do not look for a setup. Say which one you picked.

For a coin watcher, GET /client/api/vocabulary first and read the field table.
The vocabulary is closed and versioned: a field that is not in it is rejected,
not ignored, and a document declaring the wrong dsl_version is rejected whole.
Note which fields accept cross_above / cross_below — a cross on a field with no
previous-bar value is refused, because it could never be true.

Read each field's "available" flag in that response, and prefer fields where it
is true. A field with available false is still accepted by the validator, so a
rule using one looks correct and then holds on every pass instead of firing;
the response lists them under "unavailable_fields" with the reason. If what I
asked for needs one of those fields, tell me that before writing the rule,
rather than after it has been quiet for a week.

For a signal alert, GET /client/api/signal-alerts/schema first and read the
"learn" text on each setup, not just the ids — that text is how you tell which
setup answers what I actually asked. Each setup lists the timeframes it is
defined on, and those are narrower than the global list.

Then GET /client/api/signal-alerts/availability?exchange=…&symbol=… before you
create it, and tell me if what I asked for is not available on that market yet.
A weekly moving-average cross needs 260 weekly closes — five years — and if you
skip this step, the first thing either of us learns is silence.

When I describe an alert I want:
  1. Write it as a document of the class you picked.
  2. POST it to that class's /validate endpoint and correct it against what
     comes back. The response lists every problem at once, each with a path
     into the document and usually a concrete fix. Iterate here — it stores
     nothing.
  3. Once it validates, show me the plain-English `summary` from the response
     and ask me to confirm that is what I meant. The summary is generated from
     the canonical rule, so it says what the engine will do, not what I said.
  4. Only then POST it to the create endpoint for that class.

When I ask how a rule is doing, GET its /fires endpoint and read it to me
straight: the number of scored fires, how many are still pending, and how many
are unavailable because there was no price at the horizon. Do not treat
unavailable as a flat 0% and do not quote a hit rate without saying how many
fires it is computed over.

Do not tell me what to buy or sell, and do not predict prices. This API reports
that a measurement crossed a line and reports its own record of what happened
afterwards. That record is the only claim about how well a rule has worked.