@battlegrid/mcp-server 31.1.0 → 31.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,195 @@ Seeing package `31.x` alongside handshake `battlegrid@33.x` — the package **be
24
24
 
25
25
  **What this changes for you:** nothing about how you call anything. Upgrading the package no longer waits on a server deploy, and a server deploy no longer strands you on a package that names the wrong contract — reconnect and the announcement follows. **Contract breaking-change notes are no longer keyed to package versions**, since a contract move is no longer a release here; the v11-and-earlier notes below are kept as history, and the live vocabulary is always discovery.
26
26
 
27
+ ## Contract history — v37 → v48.1
28
+
29
+ Eleven majors reached authors while this section stopped at v36. That gap is the mechanism, not an
30
+ oversight: since v31 a contract move needs no release here, so nothing forced a note to be written —
31
+ and the documentation ships inside the tarball, so a note written but unpublished reaches nobody.
32
+ Both halves are now closed by a rule keyed to the *served* contract rather than to a release of this
33
+ package.
34
+
35
+ ### Changed meaning, unchanged shape
36
+
37
+ - **Three tools serve different values for identical input** (47.3.0, `derive-scan-fetch-from-report`).
38
+ The radar scan leg now derives its timeframe fetch from the strategy's **report** rather than the
39
+ on-duty agent's three perception rungs, so a required condition addressing an absolute timeframe
40
+ outside those rungs — never evaluated at scan before — now is. No field moves; the values do:
41
+
42
+ | Tool | What moves |
43
+ |---|---|
44
+ | `preview_radar_resolution` | `conditionReach[].reachReason` goes `AGENT_TIMEFRAME` → `null` |
45
+ | `get_radar_activity` | `scanReachReason` moves the same way, **on new rows only** — rows already written keep what they were recorded with |
46
+ | `get_agent_coin_qualification` | `reachReason` moves the same way; its sibling `verdict` moves `UNMEASURABLE` → a decided verdict |
47
+
48
+ **Read the last one carefully:** a client treating `UNMEASURABLE` as "this gate is switched off"
49
+ will now see that gate **BLOCK**. `AGENT_TIMEFRAME` keeps its member and narrows to the one cause
50
+ no fetch can discharge. Nothing in the payload tells you this moved.
51
+
52
+ ### Rejected input — something you author is no longer accepted
53
+
54
+ - **A benchmark-bound section no longer accepts crowd metrics or rank transforms** (49.0.0,
55
+ `fix-benchmark-legality-save-path`). On a custom section carrying a non-null `benchmarkTicker`, a
56
+ column whose metric is enrichment-stage (the `CROWD_*` family, `FLOW_ALIGN`, `SMART_RETAIL`,
57
+ `CAPTAIN_CONF`, `CONFIDENCE`, `SETTLED_AT`, the `PERP_SPOT_*` trio) is refused with
58
+ `REPORT_COLUMN_BENCHMARK_METRIC_UNSUPPORTED`, and one carrying a `rank` transform in either the
59
+ direct or the chained position with `REPORT_COLUMN_BENCHMARK_TRANSFORM_UNSUPPORTED`. Every tool
60
+ accepting a section array is affected, and no other byte of your body changes.
61
+
62
+ **Fix it by moving the column, not by retrying.** Both readings are defined *relative to the
63
+ cohort being evaluated* — a crowd reading is what this session's players did, a rank is a position
64
+ among the coins under evaluation — and a benchmark is deliberately outside that cohort. Neither
65
+ has a value there, which is why such a column could never render. Put it on an ordinary section
66
+ (`benchmarkTicker: null`), or drop it.
67
+
68
+ **This is a fix, not a new rule.** The restriction shipped with benchmark sections and was already
69
+ enforced by the column builder, by report materialization, and by `get_strategy_column_contract` —
70
+ so an author who checked a column against discovery first has been seeing this refusal all along.
71
+ What changed is that the SAVE path now asks the same question. Previously it did not, so such a
72
+ section persisted and then failed at every evaluation instead of at authoring.
73
+
74
+ - **A custom report section no longer accepts `timeframe`** (48.0.0,
75
+ `remove-section-anchor-override`). The per-section anchor override is gone. Every tool accepting a
76
+ section array — `compile_strategy_plan`, `preview_strategy_report`, `derive_strategy_rule_view`,
77
+ and `apply_strategy_plan` — refuses a section carrying it: `sections[N]: Unrecognized key(s)`,
78
+ with no other byte of your body changing. Drop the key. A section's columns resolve against the
79
+ **strategy** timeframe, and a column reaches any other timeframe by pinning it on the column
80
+ (`timeframe: { abs: '4h' }`) — which it could always do. Relative column references
81
+ (`anchor`/`lower`/`regime`) are untouched, and are the point: they track the strategy.
82
+
83
+ - **A custom report section requires `notes`** (43.0.0, `add-authored-section-notes`). Every tool
84
+ accepting a section array — `compile_strategy_plan`, `preview_strategy_report`,
85
+ `derive_strategy_rule_view` — refuses a section without it: `sections[N].notes: Required`, with no
86
+ byte of your body changing. Send explicit `null` for "no note". It is required rather than optional
87
+ because these payloads are a FULL REPLACE: an omitted key and an explicit `null` would be the same
88
+ request on the wire, so every rebuild site would silently clear a note its author wrote.
89
+ `benchmarkTicker` carries the same required-nullable discipline for the same reason.
90
+
91
+ - **Every condition requires `clock` and `closes`** (44.0.0, `add-condition-clock`), and **`exit`**
92
+ arrives with them. A condition entry now carries eight keys, not five. `clock` is `"LIVE"` (the
93
+ previous behaviour — the forming bar) or `"CLOSE"` (settled bars); `closes` is how many consecutive
94
+ closed bars must read true, 1–5, and is always `1` under LIVE. No wire default, for the same
95
+ whole-set-replacement reason as `notes`: a defaulted key would let an unrelated re-save silently
96
+ un-clock an enforced money gate back to forming-bar evidence.
97
+
98
+ A `CLOSE` clock is accepted only where a closed frame can change the reading — the clause must
99
+ resolve from the coin's own candle series at offset 0. Frame-inert operands (perp-payload scalars,
100
+ published rolling changes, ranks, zone entities, regime labels, enrichment metrics, session
101
+ scalars) are refused with `CONDITION_CLOCK_OPERAND_ILLEGAL` naming the header and the remedy: split
102
+ that clause into its own LIVE condition and `conditionRef` it. `exit: true` is legal only under
103
+ `clock: "CLOSE"` — an exit fired on a forming bar is an intrabar exit.
104
+
105
+ - **A strategy requires a seven-key `entry` object** (44.0.0 for four keys, 47.0.0 for three more —
106
+ `add-entry-on-close`, `add-level-trigger-execution`). Required on every CREATE, on
107
+ `compile_strategy_plan`, `apply_strategy_plan` and `update_intelligence_agent`. A client sending
108
+ 44.0.0's four-key object is refused under 47 with `entry.levelSource: Required` without one byte of
109
+ it changing.
110
+
111
+ ```jsonc
112
+ "entry": {
113
+ "trigger": "AT_SIGNAL", // | ON_CANDLE_CLOSE | STOP_THROUGH_LEVEL | ON_RETEST
114
+ "confirmTf": "4h", // the strategy timeframe or the rung below it — nothing else
115
+ "closes": 1, // 1–5; must be 1 unless ON_CANDLE_CLOSE
116
+ "bandAtrMultiple": 1.0, // > 0, and <= the platform's entry-deviation gate
117
+ "levelSource": "SWING_HIGH", // | SWING_LOW | BOLLINGER_UPPER | BOLLINGER_LOWER
118
+ "levelOffsetAtrMultiple": 0, // 0–2, UNSIGNED; must be 0 unless a level trigger
119
+ "validForBars": 4 // 1–24 of the strategy's own bars
120
+ }
121
+ ```
122
+
123
+ `AT_SIGNAL` with those values is byte-identical to pre-44 behaviour. The legality matrix runs one
124
+ way: all seven keys are always present, so a MEANINGFUL value under a trigger that ignores it is
125
+ refused rather than accepted-and-dropped — a dial never silently does nothing.
126
+
127
+ - **A `strategyTimeframe` the platform does not ingest is refused** (39.0.0,
128
+ `move-renderer-to-rendered-section`) on `get_coin_market_context`, where it was previously
129
+ accepted. The number is the only signal a client gets.
130
+
131
+ - **`eventType` gains `ENTRY_EXPIRED_UNCONFIRMED`** (48.1.0, `fix-entry-arming-lifecycle`) on
132
+ `get_radar_activity` and `get_radar_activity_summary` — an armed entry that reached its episode
133
+ lifetime without ever receiving a confirming close. A client holding its own closed copy of that
134
+ enum rejects the new member; one that switches exhaustively on it needs the branch.
135
+
136
+ `entryVoidCause` is deliberately **unchanged** and still carries exactly `BAND`, `CONDITIONS`,
137
+ `STRUCTURAL`. Each of those is a measurement taken *at* a close, so an episode that reached no
138
+ close gets its own event type rather than a fourth cause — `ENTRY_VOIDED` means "called off at the
139
+ close", a claim this outcome must not make.
140
+
141
+ The authoring boundary also gains a refusal with **no schema change**: a strategy declaring an
142
+ arming trigger whose `required` conditions read the `LIVE` clock is rejected on the existing
143
+ `VALIDATION_ERROR` code, naming the offending condition key. Same code, same shape, new reason —
144
+ so nothing in the published schema tells you it can now happen.
145
+
146
+ ### Removed — no alias exists
147
+
148
+ - **`get_coin_market_context` is REMOVED** (40.0.0, `retire-get-coin-market-context`). Calling it
149
+ returns an unknown-tool error. There is deliberately **no alias**: a silent redirect would hide a
150
+ payload shape change from a client that never asked for one. Use `get_market_context`.
151
+
152
+ - **`get_macd_heatmap` leaves the published surface** (41.0.0). Same shape of break, same absence of
153
+ an alias.
154
+
155
+ - **`isPrimary` is removed from every published `EvaluatedSignal`** (38.0.0) — `get_signal_log`,
156
+ `get_public_agent_signal_log_detail`, and every other tool returning a signal scorecard. Reading it
157
+ now finds the key absent rather than false.
158
+
159
+ ### Reshaped output — the same call returns a different shape
160
+
161
+ - **The normalized report section loses `timeframe`** (48.0.0, `remove-section-anchor-override`), on
162
+ every tool that publishes a strategy: `get_strategy`, `fork_strategy`, `archive_strategy`,
163
+ `restore_strategy`, `update_strategy_signal_rule`, `apply_strategy_plan`, and
164
+ `compile_strategy_plan`'s post-state. A strict parser rejects the shorter object; a lenient one
165
+ reads a section whose anchor is the strategy timeframe, which it now always is.
166
+
167
+ - **`get_strategy_column_contract` renames its anchor, both ways** (48.0.0). The request field
168
+ `sectionTimeframe` becomes `anchorTimeframe` — same meaning, and still optional. On the response,
169
+ `timeframe.requiresSectionTimeframe` becomes `requiresAnchorTimeframe`, and
170
+ `timeframe.sectionTimeframeOverrideAllowed` is **removed**: it published whether a column could go
171
+ in an anchor-overridden section, and no section can be overridden. One call also stops being
172
+ refused — a timeframe-inert metric supplied with an anchor now compiles.
173
+ `REPORT_COLUMN_SECTION_TIMEFRAME_UNSUPPORTED` leaves the `authoringCode` vocabulary.
174
+
175
+ - **`RenderedSection.notes` stops carrying provenance** (42.0.0, `separate-section-facts-from-read`),
176
+ on `preview_strategy_report`, `compile_strategy_plan` and `get_market_context`. A new REQUIRED
177
+ `provenance: string[]` carries it instead. A client reading provenance out of `notes` now reads an
178
+ author's prose, or nothing — which is a silent misread, not an error.
179
+
180
+ - **`preview_strategy_report.renderedSections[]` gains `authoredNote`** (43.0.0), the author's read
181
+ for a custom row and `null` on a platform row. Additive, but published on a `.strict()` shape.
182
+
183
+ - **`get_radar_activity` serves its evaluation curve on the FIRST PAGE ONLY** (37.0.0), and
184
+ `get_radar_activity_summary` is added. A client reading the curve off a later page finds it absent.
185
+
186
+ ### Widened enum — new members your own copy rejects
187
+
188
+ - **The authorable metric vocabulary widens by 29 keys** (46.1.0, `add-indicator-catalog-coverage`):
189
+ Keltner (`KC_UPPER`/`KC_MID`/`KC_LOWER`), Supertrend (`ST_LINE`/`ST_DIR`), Hull (`HMA20`),
190
+ WaveTrend (`WT1`/`WT2`), QQE (`QQE_RSI_MA`/`QQE_STOP`), Parabolic SAR (`PSAR`), Ichimoku
191
+ (`ICHI_CONV`/`ICHI_BASE`/`ICHI_SPAN_A`/`ICHI_SPAN_B`/`ICHI_LAG`), Williams %R (`WILLR14`),
192
+ Stochastic RSI (`STOCH_RSI14`), session pivots (`PIVOT_P`/`PIVOT_R1`–`R3`/`PIVOT_S1`–`S3`), plus
193
+ four already-published fields that became addressable: `BB_UPPER`, `BB_LOWER`, `DI_PLUS`,
194
+ `DI_MINUS`.
195
+
196
+ Additive on the wire — every request you can send today is still accepted. It is called out here
197
+ because a client holding its own closed copy of the metric enum rejects the new members, and
198
+ because **an agent holding a cached belief that these are inexpressible will substitute for a
199
+ primitive the platform now serves**. That failure raises no error at all: the author is simply told
200
+ a strategy cannot be built.
201
+
202
+ - **`EntryTrigger` gains `STOP_THROUGH_LEVEL` and `ON_RETEST`, and `EntryLevelSource` is published
203
+ for the first time** with four members (47.0.0). Additive on their own; the required keys above are
204
+ what make that bump a major.
205
+
206
+ - **Each signal-checklist item's `measured` object gains a required `triggered` boolean** (47.2.0,
207
+ `fix-entry-prompt-signal-evidence`) on `get_signal_log` and `get_public_agent_signal_log_detail`,
208
+ on the `numeric` and `categorical` arms. Additive and MINOR — a client ignoring it is unaffected —
209
+ but it is named here because a client parsing that output strictly rejects the new key, and because
210
+ leaving 47.2 unlisted would make a reader wonder what happened to it.
211
+
212
+ The `unavailable` arm deliberately does **not** carry `triggered`: "the claim could not be joined to
213
+ a stored result" and "the signal did not fire" are different states, and leaving the key off that
214
+ arm makes conflating them a type error rather than a convention.
215
+
27
216
  ## Contract history — v12 → v36
28
217
 
29
218
  > **The number in this heading is a CONTRACT version, not this package's version.** The npm badge at the top
@@ -176,6 +365,38 @@ Nothing in the proxy changes. No configuration, no environment variable, no call
176
365
 
177
366
  ### Additive in the same span
178
367
 
368
+ - **`get_regime_snapshot` publishes the evidence behind the verdict** (47.1.0,
369
+ `publish-regime-classification-evidence`). The snapshot gains `evidence`: the quantities the
370
+ classifier read, the gates it tested them against, the signed margin to the gate deciding whether
371
+ the current label survives, and the two decision facts only the classifier holds — `gateState`
372
+ (`cleared` / `held` / `dropped`) and `directionSource` (`di` / `ema`). Nothing narrows; a client
373
+ that ignores the field is unaffected.
374
+
375
+ Read `gateState` before you trust a trend label: **`held` means the ADX hysteresis buffer is
376
+ carrying the PREVIOUS bar's label rather than this bar re-confirming it** — a materially weaker
377
+ claim wearing the same word, and one no client could previously detect. `directionSource: 'ema'`
378
+ is the same shape of warning: the direction came from the fallback that fires precisely when the
379
+ DI spread is indecisive. The margin is signed so **positive always means "the current label
380
+ survives by this much"**, in every gate state, so it is safe to branch on its sign.
381
+
382
+ `conviction` is a BRANCH DISCRIMINATOR, not a confidence: it encodes *which* rule in the priority
383
+ ladder matched, not how comfortably it matched. The margins carry comfort. A client reading
384
+ conviction as a strength score is reading it wrong, and always was — this release just makes the
385
+ alternative available.
386
+
387
+ - **Thirteen metric keys join the catalog** (47.1.0) — the `regime` family gains `REGIME_STATE`,
388
+ `REGIME_CONVICTION`, `REGIME_RUN_BARS`, `REGIME_TREND_GATE`, `REGIME_TREND_MARGIN`,
389
+ `REGIME_TREND_SOURCE`, `REGIME_DI_SPREAD`, `REGIME_VOL_ATR_RATIO`, `REGIME_VOL_BBW_RATIO`,
390
+ `REGIME_MOM_BULL_VOTES`, `REGIME_MOM_BEAR_VOTES`, `REGIME_CRASH_MARGIN` and `REGIME_CRASH_LATCH`,
391
+ making the composite regime and its evidence addressable in a report column or condition for the
392
+ first time. Only a client that switches exhaustively on `MetricKey` needs new branches.
393
+
394
+ Not a contract change, but worth knowing if you author conditions: the report grammar's regime
395
+ metrics now resolve from the **confirmed close** on every path. They previously resolved from the
396
+ forming bar when a report was rendered and the confirmed close when the scan swept, so the same
397
+ condition could read differently in preview than in production. Same wire shape; same bar
398
+ everywhere now.
399
+
179
400
  `27.1.0` exit-policy authoring input on `compile_strategy_plan` · `19.2.0` `get_account_state` account identity · `19.1.0` Standing Orders marker authoring · `18.4.0` `list_gate_blocks` summary groups · `18.3.0` radar maintenance pause · `18.1.0` protection geometry · `17.2.0` break-even/trailing status · `17.1.0` `get_signal_log` condition evaluation · `13.1.0` four owner-scoped read tools · `12.1.0` cross-venue spot price metrics · `11.1.0` discoverable rate limit.
180
401
 
181
402
  ## v11 and earlier — contract history (v6 → v11)
package/dist/index.d.ts CHANGED
@@ -49,7 +49,7 @@ import { type Implementation } from '@modelcontextprotocol/sdk/types.js';
49
49
  * being asked. Move it for a change to THIS package — a proxy fix, a dependency bump, a docs
50
50
  * correction. Never move it to track the server.
51
51
  */
52
- export declare const PACKAGE_VERSION = "31.1.0";
52
+ export declare const PACKAGE_VERSION = "31.1.5";
53
53
  export declare const DEFAULT_URL = "https://mcp.battlegrid.trade/mcp";
54
54
  export interface EnvConfig {
55
55
  apiKeys: string[];
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema
52
52
  * being asked. Move it for a change to THIS package — a proxy fix, a dependency bump, a docs
53
53
  * correction. Never move it to track the server.
54
54
  */
55
- export const PACKAGE_VERSION = '31.1.0';
55
+ export const PACKAGE_VERSION = '31.1.5';
56
56
  export const DEFAULT_URL = 'https://mcp.battlegrid.trade/mcp';
57
57
  const MAX_RETRIES = 3;
58
58
  const RETRY_DELAYS_MS = [2000, 4000, 8000];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@battlegrid/mcp-server",
3
- "version": "31.1.0",
3
+ "version": "31.1.5",
4
4
  "description": "BattleGrid MCP server — play crypto prediction games from AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -88,14 +88,22 @@ values instead of the evaluated coin's — the standard way to gate a whole book
88
88
  regime. `benchmarkTicker` is required-nullable on every custom section: send `null` for an
89
89
  ordinary section, never omit it.
90
90
 
91
+ A bound section takes **indicator** columns only. Crowd/session metrics (the `CROWD_*` family,
92
+ `FLOW_ALIGN`, `SMART_RETAIL`, `CAPTAIN_CONF`, `CONFIDENCE`, `SETTLED_AT`, `PERP_SPOT_*`) and any
93
+ `rank` transform are refused there (contract 49), because both are defined relative to the cohort
94
+ being evaluated and a benchmark sits outside it — a crowd reading is what this session's players
95
+ did, a rank is a position among the coins under evaluation. Neither has a value for BTC-as-yardstick.
96
+ Put those columns on an ordinary section and `conditionRef` across.
97
+
91
98
  Budgets are served by discovery (validated today: 32 sections, 32 custom columns, 8 distinct
92
99
  timeframes, 16 conditions, 16 clauses, 16k estimated report tokens). `preview_strategy_report`
93
100
  echoes your usage against each cap.
94
101
 
95
102
  ## Conditions — deterministic logic over your own report
96
103
 
97
- Each condition: `{ conditionKey, name, definition, verdict, required }` — all five required;
98
- `verdict` and `required` have no defaults.
104
+ Each condition: `{ conditionKey, name, definition, verdict, required, exit, clock, closes }` —
105
+ all eight required, no defaults. The conditions axis saves by whole-set replacement, so an omitted
106
+ key would silently un-clock a money gate on the next unrelated edit rather than be refused.
99
107
 
100
108
  - **Clauses** compare one column: numeric/rank headers take `lt|lte|gte|gt|between`;
101
109
  classification/direction/event headers take `is|in` with the column's exact vocabulary
@@ -106,8 +114,11 @@ Each condition: `{ conditionKey, name, definition, verdict, required }` — all
106
114
  rejected, forward references are legal.
107
115
  - **Column addressing**: `{sectionKey, header}`. `sectionKey: null` is authoring sugar for a
108
116
  header unique across the whole report; a duplicated header (e.g. `ADX` in two sections) must
109
- be section-qualified. To reference your own custom sections at CREATE time, mint the
110
- `custom:<uuid>` sectionKey yourself and reuse it in the clauses.
117
+ be section-qualified. On a **CREATE you omit `sectionKey` entirely** — the server derives it
118
+ from the section itself, so the same submitted section yields the same key on every compile, and
119
+ minting a `custom:<uuid>` yourself is refused. On an UPDATE or RESTORE, send back the keys
120
+ `get_strategy` returned. To qualify a duplicated header, read the key from a preview's
121
+ `conditionColumns` or from the candidates a `CONDITION_COLUMN_AMBIGUOUS` refusal offers.
111
122
  - **Verdict**: `UP` | `DOWN` | `NEITHER` on deciding conditions, explicit `null` on building
112
123
  blocks. Declaration order is precedence: the first TRUE condition with a non-null verdict
113
124
  decides the coin's verdict. Put the more specific carrier first.
@@ -116,6 +127,48 @@ Each condition: `{ conditionKey, name, definition, verdict, required }` — all
116
127
  use it for liquidity floors, regime vetoes, and "never fight the HTF" rules.
117
128
  - Evaluation is three-valued: `UNRESOLVED` (missing input) is never collapsed to FALSE, and
118
129
  outcomes read from a still-forming bar are marked provisional.
130
+ - **The evidence clock.** `clock: "LIVE"` reads the forming bar; `clock: "CLOSE"` reads settled
131
+ bars, and `closes` (1–5) is how many consecutive closed bars must read TRUE — always `1` under
132
+ LIVE, which has exactly one frame. A CLOSE clock is legal **only** over a header resolved from
133
+ the coin's own candle series at offset 0; frame-inert operands (perp-payload scalars, published
134
+ rolling changes, ranks, zone entities, regime labels, enrichment metrics, session scalars) are
135
+ refused with `CONDITION_CLOCK_OPERAND_ILLEGAL`. The remedy is a split, not a re-clock: move that
136
+ clause into its own LIVE condition and `conditionRef` it.
137
+ - **The exit role.** `exit: true` makes a settled TRUE reading close open positions its verdict
138
+ opposes — UP exits SHORTs, DOWN exits LONGs, a `NEITHER` or `null` verdict exits both. Legal only
139
+ under `clock: "CLOSE"`, because an exit fired on a forming bar is an intrabar exit. Orthogonal to
140
+ `required`: the two act on disjoint lifecycles, pre-entry versus open.
141
+
142
+ ## Entry — when, and where, an entry is taken
143
+
144
+ `{ trigger, confirmTf, closes, bandAtrMultiple, levelSource, levelOffsetAtrMultiple, validForBars }`
145
+ — all seven required on every CREATE, no defaults. Replaced WHOLE on save, so an omitted key
146
+ reverts an author's discipline silently rather than being refused.
147
+
148
+ | `trigger` | What it does |
149
+ |---|---|
150
+ | `AT_SIGNAL` | Fire on the qualification flip, at whatever bar is on the tape. The platform's flat wall-clock entry window applies. |
151
+ | `ON_CANDLE_CLOSE` | The flip ARMS the pair; entry is taken only after a `confirmTf` close that still reads the conditions true and has not displaced beyond the band. |
152
+ | `STOP_THROUGH_LEVEL` | A TRIGGER order rests at the level ± offset; the exchange book is the watcher. |
153
+ | `ON_RETEST` | A LIMIT order rests at the broken level. Not filling is a correct outcome, not a failure. |
154
+
155
+ - `confirmTf` — exactly two values are legal: the strategy's own timeframe and the rung below it
156
+ (one only, at the ladder floor). Not the authorable main-candle set.
157
+ - `closes` — 1–5 consecutive confirming closes. Must be `1` under any trigger but `ON_CANDLE_CLOSE`.
158
+ - `bandAtrMultiple` — the veto width. The entry is VOIDED when the confirming close moved at or
159
+ beyond that many ATR against the armed verdict. Strictly `> 0` (zero voids on any adverse move,
160
+ which is a filter, not "off"), and at or below the platform's own entry-deviation gate — read it
161
+ from `get_trading_config_catalog`.
162
+ - `levelSource` — `SWING_HIGH` | `SWING_LOW` | `BOLLINGER_UPPER` | `BOLLINGER_LOWER`, resolved once
163
+ at decision time.
164
+ - `levelOffsetAtrMultiple` — 0–2, UNSIGNED. Direction is implied by the trigger and the verdict, so
165
+ a signed value would invert the trigger's meaning. Must be `0` under a non-level trigger.
166
+ - `validForBars` — 1–24 of the strategy's OWN bars, not minutes: a 1h setup waiting for a retest has
167
+ not failed after fifteen minutes.
168
+
169
+ The legality matrix runs **one way**: all seven keys are always present, so the question is never
170
+ "is it set" but "is it set to something that MEANS anything under this trigger". Leave a dial at its
171
+ inert value rather than setting one the platform will ignore.
119
172
 
120
173
  ## Signal weights — the scorecard is a weighted average, budget it
121
174
 
@@ -54,10 +54,16 @@ before any billing or LLM call:
54
54
 
55
55
  ```json
56
56
  { "conditionKey": "NO_EVENT_RISK", "name": "No liquidation tape", "verdict": null, "required": true,
57
+ "exit": false, "clock": "LIVE", "closes": 1,
57
58
  "definition": { "kind": "group", "op": "NOT", "members": [
58
59
  { "kind": "clause", "column": { "sectionKey": null, "header": "oiRegime" }, "op": "is", "label": "long liquidation" } ] } }
59
60
  ```
60
61
 
62
+ All eight keys are required on every condition — `exit`, `clock` and `closes` included. `clock`
63
+ selects the evidence frame (`LIVE` reads the forming bar, `CLOSE` reads settled bars) and `closes`
64
+ (1–5) is how many consecutive closed bars must hold; `closes` is always `1` under `LIVE`. `exit:
65
+ true` closes open positions the verdict opposes and is legal only under `clock: "CLOSE"`.
66
+
61
67
  **Quorum (N_OF).** Robust confirmation instead of a brittle ALL:
62
68
 
63
69
  ```json
@@ -72,11 +78,42 @@ directional carriers; the first TRUE carrier **in declaration order** decides
72
78
  most-specific first.
73
79
 
74
80
  **Benchmark gate.** Read regime off a `benchmarkTicker` section (all rows are the benchmark's
75
- values) and `conditionRef` it from every carrier — one place to flip the book risk-on/off.
81
+ values) and `conditionRef` it from every carrier — one place to flip the book risk-on/off. Keep that
82
+ section to indicators: crowd/session metrics and `rank` transforms are refused on a bound section
83
+ (contract 49) — rank the coins on an ordinary section and `conditionRef` the two together.
76
84
 
77
85
  **Required-count trap.** `required: true` at `allocation: 0` on a *signal rule* is rejected
78
86
  (contract 34). A required *condition* is independent of signal weights — the two gates stack.
79
87
 
88
+ ## Entry discipline recipes
89
+
90
+ The `entry` object is required on every CREATE, all seven keys, no defaults.
91
+
92
+ | Intent | Object |
93
+ |---|---|
94
+ | Today's behaviour, unchanged | `{ "trigger": "AT_SIGNAL", "confirmTf": "<strategy tf>", "closes": 1, "bandAtrMultiple": 1.0, "levelSource": "SWING_HIGH", "levelOffsetAtrMultiple": 0, "validForBars": 4 }` |
95
+ | Wait for the bar to close, skip a runaway | `{ "trigger": "ON_CANDLE_CLOSE", "confirmTf": "<strategy tf>", "closes": 1, "bandAtrMultiple": 1.0, … }` |
96
+ | Two-close confirmation on the lower rung | `{ "trigger": "ON_CANDLE_CLOSE", "confirmTf": "<lower rung>", "closes": 2, "bandAtrMultiple": 0.75, … }` |
97
+ | Break of the swing high, half an ATR through | `{ "trigger": "STOP_THROUGH_LEVEL", "levelSource": "SWING_HIGH", "levelOffsetAtrMultiple": 0.5, "validForBars": 8, "closes": 1, … }` |
98
+ | Buy the retest of the broken level | `{ "trigger": "ON_RETEST", "levelSource": "SWING_HIGH", "levelOffsetAtrMultiple": 0, "validForBars": 12, "closes": 1, … }` |
99
+
100
+ Bounds: `closes` 1–5 and `1` unless `ON_CANDLE_CLOSE`; `bandAtrMultiple` > 0 and ≤ the platform's
101
+ `defaultMaxEntryDeviationAtrMultiple`; `levelOffsetAtrMultiple` 0–2 unsigned and `0` unless a level
102
+ trigger; `validForBars` 1–24. `confirmTf` is the strategy timeframe or the rung below it, nothing
103
+ else. A meaningful value under a trigger that ignores it is REFUSED, not ignored — that is
104
+ deliberate, so a dial never silently does nothing.
105
+
106
+ ## Custom section shape
107
+
108
+ `{ kind, sectionKey, title, benchmarkTicker, notes, columns }`. `benchmarkTicker` and
109
+ `notes` are **required-nullable** — send explicit `null` rather than omitting them, because the
110
+ section is rebuilt whole on save and an omitted key clears the value. **Omit `sectionKey` on a
111
+ CREATE**: the server derives it from the section.
112
+
113
+ A section carries **no `timeframe`** (48.0.0). Its relative columns resolve against the strategy
114
+ timeframe; a column reaches any other timeframe by pinning it — `timeframe: { abs: '4h' }` on the
115
+ column, which it could always do.
116
+
80
117
  ## Weight matrices (rules)
81
118
 
82
119
  **Conviction pyramid (default).** 1–2 × Critical (the thesis signals, usually `required`),
@@ -107,6 +144,10 @@ send the full replacement object. Examples validated as canonical defaults: `vol
107
144
  | Swing breakout (4h) | 0.75 – 1.75 | 2 | 1R | 1.2R, giveback 35, buffer 0.3 | off |
108
145
  | Swing trend (4h) | 1.0 – 2.5 | 2 | 1R | 1.5R, giveback 45–55, buffer 0.3 | off |
109
146
 
147
+ Post-entry invalidation: `decisionInvalidationExitEnabled: true` closes a filled position when a
148
+ **closed** strategy-timeframe candle finishes beyond the decision's invalidation level, reduce-only.
149
+ Bar-close only — the protective stop still owns intrabar moves.
150
+
110
151
  Bounds to respect (validated): break-even trigger 0.5–2R; trailing trigger 0–2R step 0.01
111
152
  (0 = trail from entry); giveback 25–55%; buffer 0.01–1%; grace ≥ interval; stop-band floor <
112
153
  ceiling ≤ the 3×ATR structural cap; RR within the catalog's served range (0.5–3 today). Wider
@@ -37,8 +37,8 @@ closes while stops, trailing, and time decay keep managing the position intraday
37
37
 
38
38
  | TradingView script | Verdict | Port anchor |
39
39
  |---|---|---|
40
- | Squeeze Momentum [LazyBear] / TTM Squeeze | Process port (Keltner not in catalog — substitute named) | `bbWidthPct` + rank, `bollinger_squeeze`, momentum trajectory |
41
- | Supertrend / UT Bot Alerts | Split port: entry substitute + **native stop engine** | `EMA5_13`, `MAalign`, position-management trailing |
40
+ | Squeeze Momentum [LazyBear] / TTM Squeeze | **Direct port** — Keltner is native | `BB_UPPER spread KC_UPPER`, `BB_LOWER spread KC_LOWER`, `bollinger_squeeze` |
41
+ | Supertrend / UT Bot Alerts | **Direct port** for the state; flip still substituted | `ST_LINE`, `ST_DIR`, `EMA5_13` for the flip, position-management trailing |
42
42
  | Chandelier Exit | Direct port of the *mechanism* | trailing from entry (`trailingTriggerR: 0`) |
43
43
  | MACD + 200 MA filter | Direct process port | `MACD_cross`, `dist_SMA200`, `macd_bull_cross` |
44
44
  | Golden / Death Cross | Direct process port | `SMA50_SMA200_spread` + `_trend` |
@@ -46,7 +46,7 @@ closes while stops, trailing, and time decay keep managing the position intraday
46
46
  | VWAP reversion | Direct process port | `dist_VWAP`, `dist_VWAP_rank_far` |
47
47
  | Donchian / Turtle breakout | Process port (N-bar channel → swing structure) | `zone`, `dist_swingHi`, `sr_resistance_break` |
48
48
  | ICT/SMC: FVG + Order Blocks | Direct process port of the zone logic | `STRUCT_ZONES` columns + `structure_*` signals |
49
- | WaveTrend, QQE, Hull Suite, Ichimoku, Parabolic SAR, Keltner | **Not expressible** — say so; nearest neighbors below | — |
49
+ | WaveTrend, QQE, Hull Suite, Ichimoku, Parabolic SAR, Keltner | **All native since contract 46.1** — port directly | `WT1`/`WT2`, `QQE_RSI_MA`/`QQE_STOP`, `HMA20`, `ICHI_*`, `PSAR`, `KC_*` |
50
50
 
51
51
  ---
52
52
 
@@ -55,7 +55,10 @@ closes while stops, trailing, and time decay keep managing the position intraday
55
55
  **Source process.** Squeeze ON while Bollinger Bands sit inside Keltner Channels (volatility
56
56
  coiled); wait; when the squeeze releases, enter in the direction of the momentum histogram.
57
57
 
58
- **Port.** Keltner Channels are not in the catalog, so squeeze detection substitutes
58
+ **Port.** Keltner Channels are native (`KC_UPPER`/`KC_MID`/`KC_LOWER`), so the squeeze is the
59
+ source's own definition: `BB_UPPER spread KC_UPPER` negative AND `BB_LOWER spread KC_LOWER`
60
+ positive is bands-inside-Keltner. The board-relative proxy below remains serviceable but is no
61
+ longer the only option — it substitutes
59
62
  *cross-sectional and absolute* Bollinger compression — a stricter, universe-aware read:
60
63
  `bbWidthPct_rank_lo lte 10` AND `ADX lt 20` as the `SQUEEZE_ON` building block. Momentum
61
64
  direction comes from the MACD histogram trajectory (`MACD_now gt 0` with `MACD_trend rising`
@@ -75,7 +78,10 @@ Pine script simulates with a plotted line, position management executes: enable
75
78
  `trailingTriggerR: 0` (trail from entry — the Supertrend/UT Bot behavior), `giveback` as the
76
79
  ATR-offset analog (30–40 tight like factor-2 Supertrend, 45–55 loose like factor-3), plus the
77
80
  stop band (`minStopLossAtrMultiple`/`max…`) bounding the initial distance. The *flip entry*
78
- has no direct equivalent (no supertrend metric) — the named substitute is a trend-state change:
81
+ is native: `ST_DIR` carries the direction and `ST_LINE` the plotted stop. **`ST_DIR` is a
82
+ persisting state, not a flip event** — it reads the same on every bar of a trend, so the FLIP
83
+ itself still needs an event column beside it. The named substitute for the flip is a trend-state
84
+ change:
79
85
  `MAalign is "bullish"` (state) with the `EMA5_13 is "Bullish"` cross event as the trigger, and
80
86
  `ma_ema_bull_cross` / `ma_ema_aligned_bull` Critical/Important-required in `rules`. Say the
81
87
  substitution out loud when presenting the strategy.
@@ -139,7 +145,8 @@ the book out of single-name traps. Rules: `ma_sma200_above` 3 required, `ma_ema_
139
145
  **Source process.** In an uptrend (close > 200 SMA), buy panic dips (RSI(2) < 10); exit fast
140
146
  (cross of the 5-period average / a few bars).
141
147
 
142
- **Port — named substitute.** The catalog carries no RSI(2); `RSI7` is the fastest RSI series
148
+ **Port — named substitute.** The catalog carries `RSI14` and `RSI7` only, so there is no `RSI2`;
149
+ `RSI7` is the fastest RSI series
143
150
  and `lte 10` on it is a rarer, deeper panic — state that trade-off rather than hiding it.
144
151
  `DIP_BUY` verdict UP = `dist_SMA200 gt 0` (ref a required `ABOVE_200`) AND `RSI7 lte 10`.
145
152
  Rules: `rsi_oversold` 3 required `{"threshold": 25}` (RSI14 gate tuned toward the fast-dip
@@ -193,22 +200,36 @@ quorum (`N_OF(2)`: `buyPres gte 0.55`, `RVOL gte 1.2`, `closeChg gt 0`). Rules:
193
200
  `structure_fvg_approach` / `structure_ob_approach` 3/2 required (their `proximityPct` params
194
201
  are the "in the zone" dial), `structure_zone_confluence` 2, `sr_at_support` 2. Stops: tight
195
202
  band (0.5–1.5 ATR) — the zone's far edge is the invalidation, and the studio places stops by
196
- ATR/structure natively. **Not expressible, say so:** liquidity sweeps, displacement legs,
203
+ ATR/structure natively. **A grammar limit, not a missing metric** — a clause compares one column
204
+ against a literal, so an ordered sequence of events has no expressible form at all: liquidity
205
+ sweeps, displacement legs,
197
206
  killzone clocks, and Turtle-Soup false-break sequencing (ordering between events is outside
198
207
  the grammar) — offer this zone-reaction port as the nearest expressible neighbor, labelled.
199
208
 
200
- ## Not expressible — and the honest nearest neighbor
209
+ ## Now native — do not substitute for these
201
210
 
202
- | Script | Missing primitive | Nearest expressible neighbor |
211
+ WaveTrend (`WT1`/`WT2`), QQE (`QQE_RSI_MA`/`QQE_STOP`), Hull (`HMA20`), Ichimoku
212
+ (`ICHI_CONV`/`ICHI_BASE`/`ICHI_SPAN_A`/`ICHI_SPAN_B`/`ICHI_LAG`), Parabolic SAR (`PSAR`), Keltner
213
+ (`KC_UPPER`/`KC_MID`/`KC_LOWER`), daily pivots (`PIVOT_P`/`PIVOT_R1`–`R3`/`PIVOT_S1`–`S3`),
214
+ Williams %R (`WILLR14`), Stochastic RSI (`STOCH_RSI14`), Bollinger boundaries
215
+ (`BB_UPPER`/`BB_LOWER`), ADX components (`DI_PLUS`/`DI_MINUS`). All arrived with contract 46.1 —
216
+ every one of them was listed as inexpressible in this file before it.
217
+
218
+ ## Not expressible — the catalog keys this needs
219
+
220
+ The one section where a claim that the catalog LACKS something may live, and every row names the key
221
+ it denies so the claim stays checkable. A claim about the grammar's SHAPE (a clause compares one
222
+ column against a literal; `distance` rejects an `offset`) is permanent and belongs in prose above.
223
+ A claim that a metric is absent belongs here, or nowhere.
224
+
225
+ | Script / primitive | Key the catalog would need | Nearest expressible neighbour |
203
226
  |---|---|---|
204
- | WaveTrend [LazyBear] | WT oscillator | `STOCH_K`/`STOCH_D` zones + crosses (`stoch_*` signals) |
205
- | QQE / QQE MOD | smoothed-RSI ATR bands | `RSI7` trajectory + `rsi_*` signals with tuned thresholds |
206
- | Hull Suite | Hull MA | `EMA5/13/20` ribbon (`MAalign`, EMA spread trend) |
207
- | Ichimoku | cloud spans | `MAalign` + `SMA50_SMA200_spread` + `zones_htf_*` levels |
208
- | Parabolic SAR | SAR dots | trailing stop from entry (`trailingTriggerR: 0`) |
209
- | Keltner Channels | ATR envelope | `bbWidthPct` compression + `atrPct` band logic |
210
- | Pivot Points (daily) | session pivots | `VWAP` + `swingHi`/`swingLo` distances |
211
- | Previous-day / previous-week high-low (PDH/PDL) | offset-able levels (`distance` takes no `offset`; clauses are column-vs-literal) | daily swing structure: `zone_1d is "breakout high"`, `dist_swingHi_1d` / `dist_swingLo_1d` |
227
+ | RSI-2 (Connors) | `RSI2` | `RSI7 lte 10` as a named fast-dip substitute |
228
+
229
+ **PDH/PDL** stays inexpressible for a different reason, and the distinction matters: `distance`
230
+ rejects an `offset` and a clause never compares two columns, so the *grammar* has no form for it.
231
+ No metric would fix that. Use the daily swing structure (`zone_1d`, `dist_swingHi_1d`) and name the
232
+ substitution.
212
233
 
213
234
  Never compile a "port" of these under the source's name without the substitution note — a
214
235
  player who asked for WaveTrend and silently got Stochastic has no way to learn otherwise.