@battlegrid/mcp-server 31.0.2 → 31.1.0

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
@@ -20,13 +20,20 @@ From v31 the proxy reads the contract version out of the upstream handshake at c
20
20
  | Package version | This proxy's own code — a fix here, a dependency bump, a docs correction | `npm view @battlegrid/mcp-server version` |
21
21
  | Contract version | The server's wire contract, live | The stdio handshake (`battlegrid@<contract>`), or `GET /mcp/version` |
22
22
 
23
- Seeing package `31.0.0` alongside handshake `battlegrid@30.0.0` is the system working. Both are printed to stderr at startup, labelled.
23
+ Seeing package `31.x` alongside handshake `battlegrid@33.x` — the package **behind** the contract — is the system working, not a missed release. That is the pair that looks alarming and is not: the contract moved, and no release here was needed. Both numbers are printed to stderr at startup, labelled.
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 — v12 → v33
27
+ ## Contract history — v12 → v36
28
28
 
29
- These are the server contract breaks between contract 12 and contract 33. Most of the span shipped while the package sat at `11.0.0`; contract 31 landed after this package reached `31.0.0`, and the two numbers matching is coincidence — since v31 the announced contract is relayed from the server, so a package version says nothing about a contract version. They are **contract** history, not package releases: from v31 the announced contract is relayed live and a contract move is no longer a release here. Grouped by what a client observes, with the contract version that introduced each.
29
+ > **The number in this heading is a CONTRACT version, not this package's version.** The npm badge at the top
30
+ > tracks the proxy's own code; this section tracks the server's wire contract. They move independently **by
31
+ > design**: a contract move needs no release here, because a connected proxy relays the contract out of the
32
+ > upstream handshake rather than declaring it. So a package on `31.x` listing contract history up to `36.x` is
33
+ > correct — not a version someone forgot to bump. Read the live pair from the startup stderr lines or
34
+ > `GET /mcp/version`; see [Rediscovery & versioning](#rediscovery--versioning) for why.
35
+
36
+ These are the server contract breaks between contract 12 and contract 36. Most of the span shipped while the package sat at `11.0.0`; contract 31 landed after this package reached `31.0.0`, and the two numbers matching is coincidence — since v31 the announced contract is relayed from the server, so a package version says nothing about a contract version. They are **contract** history, not package releases: from v31 the announced contract is relayed live and a contract move is no longer a release here. Grouped by what a client observes, with the contract version that introduced each.
30
37
 
31
38
  **The proxy itself is unchanged.** It embeds no schemas, pins no contract version, and forwards `{ request }` verbatim. Every break below lands on whatever *authors* the payload or *reads* the result, never on the proxy.
32
39
 
@@ -38,6 +45,14 @@ These are the server contract breaks between contract 12 and contract 33. Most o
38
45
 
39
46
  ### Rejected input — something you author is no longer accepted
40
47
 
48
+ - **`upsert_deployment_policy` requires `enabled`** (35.0.0, `add-arena-deployment-pause`). The arena deployment gains an owner-owned pause, and the flag that carries it is **required, not optional** — a body accepted under contract 34 is refused under 35 without one byte of it changing. Required is the whole point: this call replaces the entire policy, so an omitted key and an explicit `true` would be the same request on the wire, and every client that rebuilt a policy without the flag would silently resume a deployment its owner had paused. Read the value from `get_deployment_policy` and send it back. **There is no separate pause verb** — pausing and resuming are this same call with the flag flipped.
49
+
50
+ Do not reach for `enabled: false` to un-deploy. It keeps every slot and stops play, which is the opposite of withdrawing: `delete_deployment_policy` is the withdrawal verb, and it discards the rules permanently. `upsert_deployment_policy` refuses an empty slot set, and its rejection names both routes.
51
+
52
+ - **A signal rule flagged `required` at allocation Off is rejected** (34.0.0, `enforce-required-allocation-invariant`). `required` and `allocation` are two independently editable fields encoding one thing — whether and how a signal participates — so `{ required: true, allocation: 0 }` was representable and meant nothing: the scorecard's triggered set already excludes Off, so such a rule could neither satisfy `minRequiredCount` nor block a trade. It is now refused on **every** rule-writing surface — `compile_strategy_plan`, `apply_strategy_plan` and `update_strategy_signal_rule` alike.
53
+
54
+ Three things make this one easy to trip over. It is an **input-acceptance narrowing**: a payload accepted under contract 33 is refused under 34 without one byte of it changing. It is **invisible in the published schema** — `zod-to-json-schema` drops effects by construction — so you cannot pre-validate it from `tools/list`, and the typed error IS the contract: read `details.inertRequiredSignalIds`, which carries the complete sorted list of offending signals, rather than the message, which names a bounded prefix. And **nothing is repaired for you**: the server will not raise the allocation (that would invent a scoring weight you never chose) nor clear the flag (that would discard your intent silently). Pick one and resend; either satisfies the boundary.
55
+
41
56
  - **`apply_strategy_plan` no longer accepts the plan** (33.0.0, `rehydrate-approved-plan-on-apply`). Its input narrows to `{ planToken, confirm }`. A `plan` member is **rejected as an unknown key** — not accepted, not ignored, and with no transitional dual shape — so every client that built the payload breaks on the next connection, which before this is what every published surface told it to do. The server keeps the plan its own compile approved and reads it back, so **you copy nothing out of the compile response**: forward `planToken` byte-for-byte and confirm.
42
57
 
43
58
  Three consequences worth knowing. The 256,000-byte cap on the apply payload is **gone with the payload**, so a large authored surface no longer becomes impossible to apply through a conversational client; the compiled plan is still capped and compile still enforces it. `PLAN_APPROVAL_NOT_FOUND` joins the error vocabulary for a token no approved plan answers to — already applied, lapsed, or never issued, all one code, because the recovery is the same in each case: compile again. And a validation refusal (a quota, a name collision, a bound agent that changed, a moved catalog) now **leaves the approved plan applicable** — clear the cause and confirm again with the same token while it lives, rather than recompiling.
@@ -87,6 +102,18 @@ These are the server contract breaks between contract 12 and contract 33. Most o
87
102
 
88
103
  ### Moved or reshaped output — a field you read is somewhere else
89
104
 
105
+ - **The signal scorecard stops serializing its entries three times over** (36.0.0, `mcp-signal-log-contents`). `get_signal_log` and `get_public_agent_signal_log_detail` drop `scorecard.triggeredSignals`, `scorecard.primarySignals` and `scorecard.supportingSignals`. Every one held the **same entry objects** `allEvaluatedSignals` already carried, so each is one filter over flags every entry still publishes:
106
+
107
+ | Removed | Read instead |
108
+ |---|---|
109
+ | `triggeredSignals` | `allEvaluatedSignals.filter(s => s.triggered)` |
110
+ | `primarySignals` | `allEvaluatedSignals.filter(s => s.triggered && s.isPrimary)` |
111
+ | `supportingSignals` | `allEvaluatedSignals.filter(s => s.triggered && !s.isPrimary)` |
112
+
113
+ **Keep the `triggered` half of those last two predicates.** Both collections were triggered-only by construction, so filtering on `isPrimary` alone surfaces signals that never fired — a silent widening, not an error. No field is removed from an entry: the key set on an `allEvaluatedSignals` member is unchanged, every evaluated signal is still returned whether or not it triggered, and `details` prose and `indicatorValues` are intact. There is no opt-in to get the three back and no default filter. Breaking only if you read one of the three names; on an 84-signal / 16-triggered log the duplication was 10,682 bytes, 28% of the scorecard, carrying no information.
114
+
115
+ - **The fleet roll-up on `list_deployment_policies` drops `unconfigured` and gains `paused`** (35.0.0, `add-arena-deployment-pause`, `fix-arena-deployment-undeploy`). `unconfigured` counted a deployment holding zero slots — a state that can no longer exist, because a stored policy now carries at least one slot and the withdrawn state is the **absence** of a policy rather than an empty one. The bucket was constant `0` at the moment of removal, so no number you read was wrong; a client reading the key still breaks on it, which is why this is a break and not a cleanup. `paused` is the owner's own switch, counted separately from `retired` — an administrator disabling the arena — because conflating them tells an owner to wait for something that will not happen. Every policy lands in exactly one bucket, so the buckets sum to `arenas`: worth asserting if you reconcile these counts.
116
+
90
117
  - **`AdminApprovedModelDTO.isActive` became `lifecycle`** (32.0.0, `remove-agent-presets`) — `AVAILABLE` / `DEPRECATED` / `RETIRED`. The boolean conflated "offered in the picker" with "bound agents may run", so there was no way to stop offering a model without hard-blocking every agent already on it. A client switching on `isActive` must switch on `lifecycle`, and **must not treat `DEPRECATED` as blocked**: that is the state which keeps bound agents running. The agent read DTO drops `brainPreset` in the same move — the marker recorded which named bundle an owner clicked, never a value the runtime read, and the model and soul it stamped are unchanged on every agent.
91
118
 
92
119
  - **`approvedPlan` is one object, not an operation union** (31.0.0, resolving #4495). It was published as a discriminated union whose discriminator does not survive JSON-Schema conversion, so what actually shipped was a bare `anyOf`: validating a failing compile response gave you every arm's errors with empty instance paths, and the top one typically complained that an UPDATE was missing `creationSeed` — a CREATE-only key — while the field that really failed went unnamed. It is now one object with a literal `operation` discriminator, and `creationSeed`, `expectedRevision` and `bindingImpact` are **required and nullable on every operation**: a CREATE plan carries a seed and `expectedRevision: null`, an UPDATE/RESTORE plan the reverse. **If you narrowed on the union arms, read `operation` instead and expect explicit `null`s rather than absent keys.** If you read those fields without narrowing, nothing changes except that they may now be null.
@@ -135,6 +162,8 @@ These are the server contract breaks between contract 12 and contract 33. Most o
135
162
 
136
163
  ### Widened enum — new members your own copy rejects
137
164
 
165
+ - **`DeploymentResolutionStatus` gains `PAUSED`** (35.0.0, `add-arena-deployment-pause`), returned by `get_deployment_policy`, `list_deployment_policies` and `preview_deployment_resolution`. A client switching exhaustively on the status must add the branch. Two properties are not obvious from the name: it is answered **before any slot is resolved**, so a paused deployment discloses no agent identity and carries `regimeUsed: null`; and its `targetSession` is nullable, because a deployment can be paused on an arena with no upcoming session and the pause is still the true answer. Do not re-derive the pause from the `enabled` flag beside it — the served status is the answer on every path, and those two disagreeing is the defect this closed.
166
+
138
167
  - **Seven plan-token failures became their own error codes** (31.0.0, resolving #4495). `TOKEN_EXPIRED`, `TOKEN_BINDING_MISMATCH`, `INVALID_TOKEN_SIGNATURE`, `INVALID_TOKEN_FORMAT`, `INVALID_TOKEN_CLAIMS`, `INVALID_DIGEST_MATERIAL` and `INVALID_MATERIALIZATION_FENCE` all used to arrive as a bare `INTERNAL_ERROR` — the server wrote the true reason to its own audit log and discarded it at the boundary, so a refused apply told you nothing. They now arrive as themselves, over MCP and HTTP alike, with 409-class status for the two state-conflict codes and 400-class for the five malformed-material codes. A client switching exhaustively on error codes must add the branches; one rendering unknown codes generically is unaffected. Two are worth handling by name: `TOKEN_EXPIRED` means recompile (the token lives five minutes), and `INVALID_TOKEN_SIGNATURE` usually means the token was not forwarded verbatim — it is opaque, so copy it byte-for-byte and never retype or reconstruct it.
139
168
 
140
169
  - `TradeEvaluationAttemptReasonCode` gains `OPEN_POSITION_CHECK_UNAVAILABLE` (19.4.0), splitting a code that previously reported a platform fault as a fact about your account.
@@ -580,12 +609,29 @@ If a run fails, inspect it before taking action. An `ENEEDAUTH` failure means th
580
609
 
581
610
  ## Skills
582
611
 
583
- Install the BattleGrid skill for AI agent instructions:
612
+ Install the BattleGrid skills for AI agent instructions:
584
613
 
585
614
  ```bash
586
615
  npx skills add playbattlegrid/battlegrid-mcp
587
616
  ```
588
617
 
618
+ Two skills ship from this repo, and both are inside the npm tarball (`SKILL.md`, `skills/`):
619
+
620
+ - **`battlegrid`** — connection, scopes, game play, and the strict compile → review → apply
621
+ strategy workflow.
622
+ - **`battlegrid-strategy-studio`** — full-power strategy authoring, for agents that would
623
+ otherwise compile bare template strategies: custom report sections and system-generated header
624
+ grammar, benchmark sections, condition trees (verdict precedence, `required` enforcement
625
+ gates, `N_OF`/`NOT` groups, condition references), tiered signal weights and the
626
+ weighted-aggregate gate math, ATR trade levels, and post-entry position management. Its
627
+ `references/` carry five validated desk-grade playbooks (volatility-compression breakout,
628
+ crowded-positioning fade, benchmark-gated relative-strength rotation, HTF trend pullback,
629
+ perp/spot flow divergence at structure), copy-adaptable recipes, and process-for-process
630
+ ports of the most popular TradingView community scripts (Squeeze Momentum [LazyBear],
631
+ Supertrend/UT Bot, Chandelier Exit, MACD + 200 MA, golden cross, RSI-2, VWAP reversion,
632
+ Donchian/Turtle, ICT FVG/order blocks) with honest named substitutions where the grammar
633
+ lacks a primitive. Shapes are binding; vocabulary stays live-discovered.
634
+
589
635
  ## License
590
636
 
591
637
  [MIT](LICENSE)
package/SKILL.md ADDED
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: battlegrid
3
+ description: MCP skill for BattleGrid — play crypto prediction games (Market Grid), author trading strategies with the strict compile → review → apply workflow, and manage strategy-bound intelligence agents from AI agents.
4
+ ---
5
+
6
+ # BattleGrid
7
+
8
+ BattleGrid is a real-time cryptocurrency prediction gaming and trading platform. This MCP server gives AI agents access to play games, author trading strategies, and run strategy-bound intelligence agents.
9
+
10
+ ## Discover the live surface first
11
+
12
+ **Tools, prompts, and resources are discovered live from this MCP connection.** Always read the current `tools/list`, `prompts/list`, and `resources/list` before acting — a cached capability list is not authoritative after a server deployment. This skill teaches the workflows and the strict request contracts; it deliberately does not copy the server's tool catalog, metric/transform vocabulary, signal IDs, formulas, or default values. Discover those from the live tools (`list_strategy_categories`, `list_strategy_vocabulary`, `list_strategy_signals`, `get_strategy_signal_definition`, …). Never guess a metric, transform, parameter, template, signal, or enabled-timeframe fact.
13
+
14
+ ## Single-account vs multi-account request shape
15
+
16
+ The strategy-authoring tools — `get_strategy_section_template`, `update_strategy_signal_rule`, `compile_strategy_plan`, `apply_strategy_plan` — use one strict server-owned envelope, `{ request: canonicalPayload }`.
17
+
18
+ - **Single account:** call them as the server publishes them, e.g. `compile_strategy_plan({ request })`.
19
+ - **Multiple accounts (proxy):** live discovery adds a sibling `account`, so the shape is exactly `{ account, request }`. Select the account in the outer field; keep `request` exactly as discovered. The proxy strips only `account` and forwards the unchanged `{ request }`.
20
+
21
+ Never put `account` inside `request`, and never flatten request fields beside it. Other tools keep whatever input shape live discovery reports for them.
22
+
23
+ ## Scopes
24
+
25
+ - `mcp:read` — strategy discovery **and** non-financial configuration writes (author strategies, edit agents, customize signals). Treat it as configuration authority, not view-only.
26
+ - `mcp:wager` — financial actions (submit paid entries, accept/cancel entry decisions, deployment policies). Enable **Server-Signed Wagers** in Profile → MCP to grant it. Pending entry decisions come from the conversational surface, which waits for approval; an agent deployed to a radar coin or a trading-enabled arena slot executes without one.
27
+
28
+ ## Author a strategy: compile → review → apply
29
+
30
+ Strategies are authored through one strict, whole-plan workflow. **Compilation changes no strategy, agent or revision; `apply_strategy_plan` is the only write to the strategy itself** — but compile is not read-only either: it parks the plan its own apply reads, and each call mints a distinct record and token, so compile once per reviewed payload and never retry or parallelise it. Review the exact returned plan before confirming.
31
+
32
+ 1. **Choose the operation and revision.**
33
+ - `list_strategies({ includeInactive? })` — visible SYSTEM and owned PRIVATE strategies with lifecycle, quota, usage, and revisions. Use `includeInactive:true` to prepare a RESTORE.
34
+ - `get_strategy({ strategyId, includeInactive? })` — the complete report, dense signal scorecard, gates, usage, and current `revision`. Thread every returned revision into the next revisioned call.
35
+ 2. **Discover the report vocabulary progressively.**
36
+ - `list_strategy_categories()` → `list_strategy_vocabulary({ category })` → `get_metric_construction_hints({ metric })` → `get_strategy_column_contract({ column, sectionTimeframe? })`.
37
+ - `get_strategy_section_template({ request })` for a listed template; `preview_strategy_report(payload)` for a point-in-time rendered preview.
38
+ 3. **Discover signals at the strategy timeframe.**
39
+ - `list_strategy_signals({ module?, query? })` → `get_strategy_signal_definition({ signalId, timeframe })`. Availability is structural, not a promise of a live trigger.
40
+ 4. **(Optional) review draft-only guidance.**
41
+ - `derive_strategy_rule_view({ sections, rules? })` returns report perception, server defaults, and suggestions without reading or writing a strategy. Suggestions/resets only shape the next plan input; they persist only through `apply_strategy_plan`.
42
+ 5. **Compile one complete plan.**
43
+ - `compile_strategy_plan({ request })`, where the nested request contains exactly one strict branch plus a bounded `coinSelection`, `intentSummary`, and `assumptions`:
44
+ - **CREATE** — supplies the full new aggregate.
45
+ - **UPDATE** — supplies at least one changed axis and `expectedRevision`. **Send only the
46
+ axes that change.** The server preserves every axis you omit and every signal you do not
47
+ name, and it re-derives the complete post-state, scorecard and diff itself. Restating the
48
+ whole post-state changes nothing about the result and the account pays for those tokens on
49
+ this call and on every later step of the conversation. Three fields are *not* axes and are
50
+ required on every compile regardless — `intentSummary`, `assumptions` and `coinSelection`;
51
+ omitting one is a typed error, not a saving.
52
+ - **RESTORE** — targets an owned inactive revision and may include repair axes.
53
+ - Signal overrides are sparse: an omitted signal/axis stays unchanged; omitted `params` preserves canonical params byte-for-byte; present `params` replaces them only after strict validation.
54
+ - **`required` needs a weight.** A rule with `required: true` at `allocation: 0` is rejected (contract 34): the scorecard's triggered set excludes Off, so the flag could satisfy no gate and block no trade. Either raise the allocation or leave `required: false` — the server picks neither for you. The refusal names every offending signal in `details.inertRequiredSignalIds`, and it can fire on rules you did not touch: a strategy authored before contract 34 may hold the pair, and the merged post-state is what gets checked.
55
+ 6. **Review before confirming.**
56
+ - `approvedPlan` — complete post-state, proposed revision, dense scorecard, viability, canonical diff, expiry, and bound-agent impact.
57
+ - `reviewContext` — exact column contracts, point-in-time report preview and coin scope, open-position observation, and provisional quota/name admission (advisory until the write). Open positions are awareness only and do not block an edit.
58
+ - The plan token expires after five minutes. Recompile after expiry, catalog drift, revision drift, or a changed bound-agent fence.
59
+ 7. **Apply the plan the server already holds.**
60
+ - After explicit user approval: `apply_strategy_plan({ request: { planToken, confirm: true } })`. **There is no `plan` member** — one is rejected as an unknown key. The server keeps the plan its own compile approved and reads it back, so nothing is copied from the compile response and nothing can be mistyped, truncated or half-reconstructed in transit.
61
+ - Forward `planToken` byte-for-byte exactly as compile returned it. It is an opaque signed value: never retyped, paraphrased, abbreviated or rebuilt from memory. A mangled token addresses no approved plan and is refused.
62
+ - Refusals and their recoveries: `PLAN_APPROVAL_NOT_FOUND` means no approved plan answers to this token — it was already applied, it lapsed, or it was never issued; compile again. `TOKEN_EXPIRED` means the five minutes ran out; compile again. Anything else — a quota, a name collision, a bound agent that changed, a moved catalog — **leaves the plan applicable**: clear the cause and confirm again with the same token while it lives.
63
+ - Changed configuration reaches every bound agent immediately. Report the returned strategy, committed revision, changed axes and applied impact exactly as given, and use that returned revision for the next mutation.
64
+
65
+ **Focused edits & lifecycle:** `update_strategy_signal_rule({ request })` is the thin one-rule edit (requires `required`; omit `params` to preserve them; `required: true` needs `allocation > 0`). `fork_strategy` requires `sourceRevision`; `archive_strategy` requires `expectedRevision` and `confirm:true`; `restore_strategy` is only the thin unchanged-content path — if it reports `REPAIR_REQUIRED`, use the RESTORE compile/review/apply flow instead.
66
+
67
+ **Author the full surface, not a template.** A bare compile — a few platform sections, no conditions, untouched signal weights — uses a fraction of the studio: the strategy aggregate also owns typed conditions (verdicts + `required` enforcement gates), tiered signal allocations with per-signal params, routing gates (`minAggregateScore`, `minRequiredCount`, `minAtrPct`), the ATR stop band + risk-reward floor, post-entry position management (break-even / trailing / time-decay), and marker-bearing Market Read prose. The companion **`battlegrid-strategy-studio` skill** (shipped beside this one) carries the capability map plus validated desk-grade playbooks and recipes for all of it — activate it whenever a strategy is being created or upgraded.
68
+
69
+ ## Strategy-bound agents
70
+
71
+ Bind a strategy to an agent at creation time — there is no direct strategy-creation tool.
72
+
73
+ 1. `list_approved_models()` — valid `modelId` values for agent creation/update.
74
+ 2. `list_strategies()` — pick the `strategyId` to bind.
75
+ 3. `create_intelligence_agent({ …, modelId, strategyId })` — create a strategy-bound agent (avatar is server-minted).
76
+ 4. `update_intelligence_agent({ agentId, … })` — update config; rebinding via `strategyId` requires `confirm:true`.
77
+ 5. `get_agent_journal({ agentId })` / `get_agent_automation_status({ agentId })` — monitor performance and deployments.
78
+
79
+ ## Play a game (Market Grid)
80
+
81
+ Predict UP or DOWN for each coin in the pool; exactly one coin is your **Captain** (2x score multiplier). Drive it from the live tools:
82
+
83
+ 1. `get_account_state()` — balance, rank, agent slots, wager status.
84
+ 2. `list_market_grid_sessions({ status: "PENDING" })` — find an open game (a `$0` entry fee is risk-free).
85
+ 3. `get_market_grid_session({ sessionId })` — coin pool, timeframe, payout structure.
86
+ 4. `get_market_context({ sessionId })` — indicators, rankings, and trends for the session.
87
+ 5. `check_market_grid_submission({ sessionId })` — avoid duplicate submissions (`update_market_grid` to modify).
88
+ 6. `submit_market_grid({ sessionId, grid, reasoning, confidenceScore, modelName, pickReasoning })` — submit predictions.
89
+ 7. `get_market_grid_results({ sessionId })` — results once the session is `SETTLED`.
90
+
91
+ **Grid validation:** grid size matches the coin pool; each coin appears once; positions are sequential (0,1,2,…); exactly one cell is `isCaptain: true`.
92
+
93
+ The `play-market-grid` prompt (discover via `prompts/list`) provides a guided end-to-end workflow.
94
+
95
+ ## Retired operations
96
+
97
+ `create_strategy` (and other legacy direct-authoring/agent-scoped rule tools) are **retired** — they are absent from discovery and cannot be invoked. Author strategies with compile → review → apply, edit rules with `update_strategy_signal_rule`, and bind strategies to agents at agent creation. Do not attempt flat legacy payloads; the server enforces a closed-world request root and the proxy never reconstructs them.
98
+
99
+ ## Common errors
100
+
101
+ | Error | Cause | Fix |
102
+ |-------|-------|-----|
103
+ | `BATTLEGRID_API_KEY is required` | Missing API key | Set `BATTLEGRID_API_KEY` (or `BATTLEGRID_API_KEYS`) |
104
+ | `API key must start with "bg_live_"` | Invalid key format | Generate a new key at battlegrid.trade → Profile → MCP |
105
+ | Authentication failed (401/403) | Key revoked/rotated | Generate a new key and **restart** the proxy (keys read once at startup) |
106
+ | `"account" parameter is required` | Multi-account call missing `account` | Add the outer `account`; keep `request` unchanged |
107
+ | Plan token expired / revision drift | >5 min since compile, or upstream changed | Recompile, review the fresh plan, then apply |
108
+ | Required at allocation Off | A rule flags `required` on a signal weighted `0` (contract 34) | Read `details.inertRequiredSignalIds`; per signal either raise `allocation` or set `required: false` |
109
+ | Method not found | Calling a retired/unknown tool | Re-run `tools/list`; use the compile → review → apply flow |
110
+ | `Wager scope required` | `mcp:wager` not enabled | Enable Server-Signed Wagers in Profile → MCP |
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.0.2";
52
+ export declare const PACKAGE_VERSION = "31.1.0";
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.0.2';
55
+ export const PACKAGE_VERSION = '31.1.0';
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.0.2",
3
+ "version": "31.1.0",
4
4
  "description": "BattleGrid MCP server — play crypto prediction games from AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,9 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "README.md",
13
- "LICENSE"
13
+ "LICENSE",
14
+ "SKILL.md",
15
+ "skills"
14
16
  ],
15
17
  "scripts": {
16
18
  "build": "tsc",
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: battlegrid-strategy-studio
3
+ description: Author full-power BattleGrid trading strategies over MCP — multi-section reports, benchmark sections, layered conditions with verdicts and enforcement gates, tiered signal weights, routing gates, ATR trade levels, and post-entry position management. Use whenever building or upgrading a strategy so it uses the whole studio, not a bare template. Companion to the `battlegrid` skill, which owns connection and the compile → review → apply workflow.
4
+ ---
5
+
6
+ # BattleGrid Strategy Studio — full-power authoring
7
+
8
+ A default strategy — a few platform sections, no conditions, untouched weights — wastes the
9
+ studio. This skill teaches every axis the strategy aggregate owns and how professional desks
10
+ compose them. Workflow, envelopes, and error recovery live in the `battlegrid` skill; this one
11
+ is about *what to author*.
12
+
13
+ **Ground rule: shapes here are binding, tokens are illustrative.** Every metric code, signal id,
14
+ header, and bound in this skill was validated against the live server, but the server's
15
+ vocabulary moves with deployments. Before compiling, re-discover (`list_strategy_categories` →
16
+ `list_strategy_vocabulary` → `get_metric_construction_hints` → `get_strategy_column_contract`,
17
+ `list_strategy_signals` → `get_strategy_signal_definition`) and prefer what discovery returns
18
+ over anything printed here.
19
+
20
+ ## What a strategy owns (author all of it, deliberately)
21
+
22
+ | Axis | Fields | What it does |
23
+ |---|---|---|
24
+ | Identity | `name` ≤50, `tagline` ≤80, `description` ≤500 | How agents and humans find it |
25
+ | Timeframe | `timeframe` (from discovery's enabled list) | Anchor rung; cadence persona and regime rung derive from it |
26
+ | Report | `sections[]` — platform + custom columns | The per-coin table the agent LLM actually reads |
27
+ | Conditions | `conditions[]` — typed boolean trees | Deterministic verdicts (UP/DOWN/NEITHER) + hard enforcement gates |
28
+ | Signal rules | `rules[]` — `{signalId, allocation 0–3, required, params?}` | What scores, how much it counts, what must fire |
29
+ | Routing gates | `minAggregateScore` 0–1, `minRequiredCount` 0–20, `minAtrPct` | Whether a scored setup may route to a trade |
30
+ | Trade levels | `minStopLossAtrMultiple` < `maxStopLossAtrMultiple`, `minRiskRewardRatio` | Where stops/targets may sit |
31
+ | Position management | breakEven / trailing / timeDecay dials | How the stop moves after entry |
32
+ | Market Read | `marketReadText` ≤2000 with `{...}` markers | Standing orders rendered with live values |
33
+
34
+ A compile also always carries `intentSummary`, `assumptions[]`, and `coinSelection` — call
35
+ context, not strategy state (see the `battlegrid` skill).
36
+
37
+ ## The full-power checklist
38
+
39
+ Before compiling a CREATE, confirm all six; a "no" is a decision, not an omission:
40
+
41
+ 1. Report has at least one **custom section** whose columns encode the thesis (not only platform
42
+ modules), and every column earns its tokens.
43
+ 2. **Conditions** encode the entry logic deterministically — building blocks + verdict carriers —
44
+ and at least one `required: true` condition guards spend on obvious disqualifiers.
45
+ 3. **Every signal you want scoring is named in `rules`** with a deliberate tier; signals you do
46
+ not name keep server defaults (typically Off). Verify in the compiled scorecard, never assume.
47
+ 4. **Gates** are set against the weight budget you chose (see scoring math below).
48
+ 5. **Trade levels + position management** match the setup's geometry and holding period.
49
+ 6. `marketReadText` states the standing orders with markers so the agent sees live values inline.
50
+
51
+ ## Report grammar — sections and columns
52
+
53
+ A custom column is `metric × transform (± chained transform) × timeframe ref (± params)`.
54
+ Headers are **system-generated, never named by you**. Validated affix patterns:
55
+
56
+ | Transform | Header shape | Example (validated) |
57
+ |---|---|---|
58
+ | `value` | `<code>` | `bbWidthPct`, `RVOL`, `rate` |
59
+ | `trajectory` (window 4) | `<code>_t3 … _t1`, `<code>_now`, `<code>_trend` (rising/falling/flat) | `RSI14_now`, `RSI14_trend` |
60
+ | `distance` (price → level) | `dist_<code>` (signed %) | `dist_SMA50` |
61
+ | `spread` (base vs operand) | `<base>_<operand>_spread` | `mark_oracle_spread`, `EMA5_EMA13_spread` |
62
+ | `aggregate` (window N) | `<code>_mean<N>` | `rate_mean24` |
63
+ | `rank` (ordering hi/lo/far/near) | `<code>_rank_<ordering>` — ordinal, 1 = best; compare with `lte N` for top-N | `bbWidthPct_rank_lo`, `closeChg_rank_far` |
64
+ | `efficiency` (window N) | `<code>_er` (0–1; 1 = straight move, ~0 = chop) | `close_ltf_er` |
65
+ | `maxShare` (window N) | `<code>_maxShare` (0–1 concentration) | `volBase_ltf_maxShare` |
66
+ | `classifyZone` / `classifyState` | `<code>_zone` / `<code>_state` | `RSI14_zone` (overbought/oversold/neutral), `ADX_state` (weak/developing/trending/extreme) |
67
+
68
+ Non-anchor rungs add a rung affix: `_ltf` (lower) / `_htf` (regime), e.g. `MAalign_htf`,
69
+ `zones_htf_support_dist`. Chains are bounded at two: inner `distance`/`spread` → outer
70
+ `trajectory`/`aggregate`/`efficiency`/`maxShare`/`rank` (e.g. EMA5 `spread` EMA13 ×
71
+ `trajectory` → `EMA5_EMA13_spread_now` + `_trend`). Read exact headers from
72
+ `get_strategy_column_contract` or a `preview_strategy_report`'s `conditionColumns` before
73
+ writing conditions against them.
74
+
75
+ **Timeframe references — two families.** *Relative* (`{rel: "anchor" | "lower" | "regime"}`)
76
+ re-resolve when the strategy timeframe changes; `regime` is the anchor's ladder successor (a 4h
77
+ anchor's regime rung is 1d today). *Pinned* (`{abs: "<tf>"}`) is fixed and ignores anchor
78
+ retunes; its legal set is discovery's `rankedTimeframes` — a **superset** of the authorable
79
+ anchor set (`timeframes`), so `{abs: "1d"}` is valid while `1d` is not an anchor. Pinned
80
+ headers suffix the literal: `RSI14_1d`, `dist_SMA200_1d`, `MAalign_1d` (validated). `offset: 1`
81
+ on a pinned `value` column reads the **last closed** bar of that timeframe — the deterministic
82
+ daily-close read; offset does not change the header, so one section carries one offset per
83
+ `metric × timeframe`. This is how higher-timeframe theses (daily-chart strategies included)
84
+ are authored on an intraday anchor — see the daily pattern in `references/tradingview-ports.md`.
85
+
86
+ **Benchmark sections** (`benchmarkTicker: "BTC"` on a custom section) read the *benchmark's*
87
+ values instead of the evaluated coin's — the standard way to gate a whole book on market-leader
88
+ regime. `benchmarkTicker` is required-nullable on every custom section: send `null` for an
89
+ ordinary section, never omit it.
90
+
91
+ Budgets are served by discovery (validated today: 32 sections, 32 custom columns, 8 distinct
92
+ timeframes, 16 conditions, 16 clauses, 16k estimated report tokens). `preview_strategy_report`
93
+ echoes your usage against each cap.
94
+
95
+ ## Conditions — deterministic logic over your own report
96
+
97
+ Each condition: `{ conditionKey, name, definition, verdict, required }` — all five required;
98
+ `verdict` and `required` have no defaults.
99
+
100
+ - **Clauses** compare one column: numeric/rank headers take `lt|lte|gte|gt|between`;
101
+ classification/direction/event headers take `is|in` with the column's exact vocabulary
102
+ (served per header in `conditionColumns` / the column contract).
103
+ - **Groups**: `ALL`, `ANY`, `NOT`, `N_OF` (with `n`), depth ≤ 2 (an inner group holds leaves
104
+ only).
105
+ - **References** (`{kind:"conditionRef", conditionKey}`) compose named conditions; cycles are
106
+ rejected, forward references are legal.
107
+ - **Column addressing**: `{sectionKey, header}`. `sectionKey: null` is authoring sugar for a
108
+ 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.
111
+ - **Verdict**: `UP` | `DOWN` | `NEITHER` on deciding conditions, explicit `null` on building
112
+ blocks. Declaration order is precedence: the first TRUE condition with a non-null verdict
113
+ decides the coin's verdict. Put the more specific carrier first.
114
+ - **`required: true`** makes a FALSE reading a hard gate: the compose-trade evaluation is
115
+ blocked *before any billing or LLM call*. This is the cheapest risk control in the studio —
116
+ use it for liquidity floors, regime vetoes, and "never fight the HTF" rules.
117
+ - Evaluation is three-valued: `UNRESOLVED` (missing input) is never collapsed to FALSE, and
118
+ outcomes read from a still-forming bar are marked provisional.
119
+
120
+ ## Signal weights — the scorecard is a weighted average, budget it
121
+
122
+ Allocation tiers: `0` Off, `1` Normal, `2` Important, `3` Critical.
123
+
124
+ ```
125
+ aggregateScore = Σ(score × allocation) / Σ(allocation) over triggered signals
126
+ ```
127
+
128
+ Consequences worth designing around:
129
+
130
+ - Weights are **relative**: one Critical among Normals dominates; all-Critical equals all-Normal.
131
+ Build a pyramid — 1–2 Critical (the thesis), 2–4 Important (confirmation), a few Normal
132
+ (context) — and turn everything else Off so noise cannot dilute the average.
133
+ - `required: true` on a rule does two things: the signal counts toward `minRequiredCount` when
134
+ it triggers, and the gate blocks routing when too few required signals fired. A rule with
135
+ `required: true` at allocation 0 is **rejected** (contract 34) — raise the allocation or clear
136
+ the flag.
137
+ - `params` are per-signal and replace canonical defaults only when present and valid — tune
138
+ thresholds to the strategy (e.g. an RSI-overbought at 65 for a fade book) after reading
139
+ `get_strategy_signal_definition({signalId, timeframe})`. Omitted `params` preserve defaults
140
+ byte-for-byte.
141
+ - Which signals *can* trigger follows from your report's sections/columns
142
+ (`derive_strategy_rule_view` shows in-report membership for a draft). Weighting a signal your
143
+ report never feeds is dead weight.
144
+
145
+ ## Routing gates
146
+
147
+ - `minAggregateScore` (0–1): floor on the weighted average above. Set it from your pyramid: if
148
+ routing should need the Critical thesis plus one Important confirmation, compute that mix's
149
+ aggregate and gate just under it.
150
+ - `minRequiredCount` (0–20): how many `required` signals must be among the triggered set.
151
+ - `minAtrPct`: minimum ATR as % of price — a dead-market filter; bounds come from
152
+ `get_trading_config_catalog` (validated today: 0.1–10).
153
+
154
+ ## Trade levels (ATR geometry)
155
+
156
+ `minStopLossAtrMultiple < maxStopLossAtrMultiple` (band where the stop may sit; ceiling capped
157
+ at the structural 3×ATR), `minRiskRewardRatio` (catalog bounds today: 0.5–3). Position size is
158
+ risk-budget based (`riskPct / stopDistancePct` — see the `battlegrid` skill's contract notes),
159
+ so a *wider* stop means a *smaller* position, not more risk. Tight bands suit breakout entries;
160
+ wide bands suit mean reversion that needs room.
161
+
162
+ ## Position management (how the stop moves)
163
+
164
+ Validated live bounds: `breakEvenTriggerR` 0.5–2 · `trailingTriggerR` 0–2 step 0.01 (0 = trail
165
+ from entry) · `trailingGivebackPct` 25–55 · `trailingBufferPct` 0.01–1 · `timeDecay` grace
166
+ 1–1440 min ≥ interval 1–480 min, tighten 0.1–50%, max 1–100%, stale threshold 0–100% of TP
167
+ progress. Each mechanism has its own enabled flag; there is no umbrella switch. Trend books:
168
+ arm break-even ~1R, trail late with a generous giveback (40–55). Mean-reversion/scalp books:
169
+ break-even early, tight giveback, and **timeDecay on** — a thesis that hasn't paid within its
170
+ horizon should be squeezed out.
171
+
172
+ ## Coin selection (per compile, not persisted)
173
+
174
+ `{mode:"ranked", limit, category?}` (categories today: ALL, CRYPTO, L1, MEMES, DEFI, TRADFI,
175
+ STOCKS, INDICES, COMMODITIES) or `{mode:"explicit", tickers[]}`. Choose the cohort the review
176
+ should render over — explicit tickers for a focused edit, ranked for a scanning book.
177
+
178
+ ## Market Read markers
179
+
180
+ `marketReadText` renders with live values wherever a `{...}` marker names a column header
181
+ (`{RVOL}`), a condition (`{SQUEEZE_ON}` — renders outcome plus evidence), or a section-qualified
182
+ form on collision (`{custom:<uuid>.MAalign}`). The preview returns `marketReadMarkers` with each
183
+ marker's resolution status — fix `unknown`/`ambiguous` markers before compiling.
184
+
185
+ ## References
186
+
187
+ - `references/playbooks.md` — five validated desk-grade playbooks with full payloads: volatility
188
+ compression breakout, crowded-positioning fade, relative-strength rotation with a benchmark
189
+ gate, HTF trend pullback, perp/spot flow divergence at structure.
190
+ - `references/recipes.md` — copy-adaptable column recipes, condition patterns, weight matrices,
191
+ and trade-level/position-management presets per trading persona.
192
+ - `references/tradingview-ports.md` — the most popular TradingView community scripts (Squeeze
193
+ Momentum [LazyBear], Supertrend/UT Bot, Chandelier Exit, MACD + 200 MA, golden cross, RSI-2,
194
+ VWAP reversion, Donchian/Turtle, ICT FVG/order blocks) translated process-for-process onto
195
+ the studio's vocabulary, with an expressibility triage and honest named substitutions for
196
+ what the grammar cannot carry.
197
+
198
+ Validate a draft cheaply before compiling: `derive_strategy_rule_view` (report membership +
199
+ rule defaults, no write) and `preview_strategy_report` (rendered tables, condition outcomes
200
+ with evidence, verdict tally, budget usage, marker resolution). Then compile once, review the
201
+ compiled truth, and apply.
@@ -0,0 +1,370 @@
1
+ # Strategy Studio playbooks — validated desk-grade examples
2
+
3
+ Five complete composition patterns, modeled on how systematic desks actually structure these
4
+ trades. Every header, vocabulary label, bound, and shape below was validated against the live
5
+ server (preview + column contracts) on 2026-08-28. Tokens are still illustrative: re-discover
6
+ before compiling, and read exact headers from `preview_strategy_report`'s `conditionColumns`.
7
+
8
+ Playbook 1 shows the **full** `compile_strategy_plan` CREATE request. Playbooks 2–5 show only
9
+ the axes that differ — their envelope (`operation`, `intentSummary`, `assumptions`,
10
+ `coinSelection`, identity, timeframe) follows the same shape. Multi-account proxies wrap each as
11
+ `{ account, request }`.
12
+
13
+ ---
14
+
15
+ ## 1 · Volatility Compression Breakout (vol desk, 4h swing)
16
+
17
+ **Thesis.** Coins in the tightest Bollinger compression on the board, with trend strength not yet
18
+ developed, break hard when volume arrives. Trade the expansion, direction decided by the break.
19
+
20
+ **Why this shape.** Cross-sectional `rank` finds compression *relative to the whole universe*
21
+ (cheaper and more robust than an absolute width threshold); a building-block condition holds the
22
+ squeeze state and two verdict carriers decide direction; a required liquidity floor stops the
23
+ agent spending calls on illiquid names; tight stop band + trailing because breakout invalidation
24
+ is nearby and winners should run.
25
+
26
+ ```json
27
+ {
28
+ "request": {
29
+ "operation": "CREATE",
30
+ "intentSummary": "Cross-sectional volatility-compression breakout: enter expansions out of the tightest Bollinger squeezes on the board, direction decided by the breaking bar, volume-confirmed.",
31
+ "assumptions": [
32
+ "4h anchor: swing cadence, structural zones read on the regime rung",
33
+ "Ranked CRYPTO cohort approximates the live tradable universe"
34
+ ],
35
+ "coinSelection": { "mode": "ranked", "limit": 20, "category": "CRYPTO" },
36
+ "name": "Compression Breakout",
37
+ "tagline": "Tightest bands on the board, traded on expansion",
38
+ "description": "Scans the universe for Bollinger-width compression (bottom-decile rank), requires real participation, and enters the expansion bar in its own direction. Structure zones frame targets.",
39
+ "timeframe": "4h",
40
+ "sections": [
41
+ {
42
+ "kind": "custom",
43
+ "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5",
44
+ "title": "Squeeze Scan",
45
+ "benchmarkTicker": null,
46
+ "columns": [
47
+ { "metric": "BB_WIDTH_PCT", "transformId": "value", "timeframe": { "rel": "anchor" } },
48
+ { "metric": "BB_WIDTH_PCT", "transformId": "rank", "timeframe": { "rel": "anchor" }, "ordering": "lo" },
49
+ { "metric": "ADX", "transformId": "value", "timeframe": { "rel": "anchor" } },
50
+ { "metric": "RVOL", "transformId": "value", "timeframe": { "rel": "anchor" } },
51
+ { "metric": "BB_PCT_B", "transformId": "value", "timeframe": { "rel": "anchor" } },
52
+ { "metric": "CLOSE_CHANGE", "transformId": "value", "timeframe": { "rel": "anchor" } },
53
+ { "metric": "NOTIONAL_VOLUME_1D", "transformId": "value", "timeframe": { "rel": "anchor" } }
54
+ ]
55
+ },
56
+ { "kind": "platform", "sectionKey": "includeBollingerBands" },
57
+ { "kind": "platform", "sectionKey": "includeStructureZones" }
58
+ ],
59
+ "conditions": [
60
+ {
61
+ "conditionKey": "LIQUID_FLOOR",
62
+ "name": "Liquidity floor",
63
+ "definition": { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "vol24hUsd" }, "op": "gte", "value": 25000000 },
64
+ "verdict": null,
65
+ "required": true
66
+ },
67
+ {
68
+ "conditionKey": "SQUEEZE_ON",
69
+ "name": "Volatility compression",
70
+ "definition": {
71
+ "kind": "group", "op": "ALL", "members": [
72
+ { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "bbWidthPct_rank_lo" }, "op": "lte", "value": 10 },
73
+ { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "ADX" }, "op": "lt", "value": 20 }
74
+ ]
75
+ },
76
+ "verdict": null,
77
+ "required": false
78
+ },
79
+ {
80
+ "conditionKey": "BREAK_UP",
81
+ "name": "Upside expansion",
82
+ "definition": {
83
+ "kind": "group", "op": "ALL", "members": [
84
+ { "kind": "conditionRef", "conditionKey": "SQUEEZE_ON" },
85
+ { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "closeChg" }, "op": "gt", "value": 0 },
86
+ { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "RVOL" }, "op": "gte", "value": 1.5 },
87
+ { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "pctB" }, "op": "gte", "value": 0.85 }
88
+ ]
89
+ },
90
+ "verdict": "UP",
91
+ "required": false
92
+ },
93
+ {
94
+ "conditionKey": "BREAK_DOWN",
95
+ "name": "Downside expansion",
96
+ "definition": {
97
+ "kind": "group", "op": "ALL", "members": [
98
+ { "kind": "conditionRef", "conditionKey": "SQUEEZE_ON" },
99
+ { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "closeChg" }, "op": "lt", "value": 0 },
100
+ { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "RVOL" }, "op": "gte", "value": 1.5 },
101
+ { "kind": "clause", "column": { "sectionKey": "custom:a1a1a1a1-b2b2-4c3c-8d4d-e5e5e5e5e5e5", "header": "pctB" }, "op": "lte", "value": 0.15 }
102
+ ]
103
+ },
104
+ "verdict": "DOWN",
105
+ "required": false
106
+ }
107
+ ],
108
+ "rules": [
109
+ { "signalId": "bollinger_squeeze", "allocation": 3, "required": true },
110
+ { "signalId": "volume_surge", "allocation": 2, "required": true, "params": { "multiplier": 1.5 } },
111
+ { "signalId": "volatility_atr_expanding", "allocation": 2, "required": false },
112
+ { "signalId": "sr_resistance_break", "allocation": 2, "required": false },
113
+ { "signalId": "sr_support_break", "allocation": 2, "required": false },
114
+ { "signalId": "bollinger_upper_touch", "allocation": 1, "required": false },
115
+ { "signalId": "bollinger_lower_touch", "allocation": 1, "required": false },
116
+ { "signalId": "trend_adx_ranging", "allocation": 1, "required": false }
117
+ ],
118
+ "minAggregateScore": 0.55,
119
+ "minRequiredCount": 2,
120
+ "minAtrPct": 0.8,
121
+ "minStopLossAtrMultiple": 0.75,
122
+ "maxStopLossAtrMultiple": 1.75,
123
+ "minRiskRewardRatio": 2,
124
+ "breakEvenEnabled": true,
125
+ "breakEvenTriggerR": 1,
126
+ "trailingEnabled": true,
127
+ "trailingTriggerR": 1.2,
128
+ "trailingGivebackPct": 35,
129
+ "trailingBufferPct": 0.3,
130
+ "timeDecayEnabled": false,
131
+ "marketReadText": "Trade only expansions out of compression: {SQUEEZE_ON} must read TRUE on the setup bar. Board standing {bbWidthPct_rank_lo}; participation {RVOL} must be at least 1.5x. Longs on {BREAK_UP}, shorts on {BREAK_DOWN}; skip anything failing {LIQUID_FLOOR}. Frame targets against the regime-rung zones ({zones_htf_resist_dist} up, {zones_htf_support_dist} down)."
132
+ }
133
+ }
134
+ ```
135
+
136
+ **Review focus.** In the compile response: the scorecard should show exactly the eight named
137
+ rules scoring (everything else Off); `reviewContext.reportPreview.conditionOutcomes` should show
138
+ `SQUEEZE_ON` evidence per coin and the verdict tally; check `marketReadMarkers` all resolve.
139
+
140
+ ---
141
+
142
+ ## 2 · Crowded-Positioning Fade (derivatives desk, 1h intraday)
143
+
144
+ **Thesis.** When funding is paying one side heavily, open interest is building, and the OI·price
145
+ regime says the crowd is adding into an extended move, fade it back toward equilibrium.
146
+
147
+ **Sections.** Custom `Positioning` (mint key `custom:b2b2…`):
148
+ `FUNDING_RATE value` → `rate` · `FUNDING_RATE aggregate w24` → `rate_mean24` (one venue print is
149
+ noise; the 24-sample mean is the crowd's bill) · `FUNDING_ANN value` → `ann` · `OI_CHG value` →
150
+ `oiChg` · `OI_PX_REGIME value` → `oiRegime` (vocab: `new longs` / `new shorts` /
151
+ `short covering` / `long liquidation`) · `MARK spread ORACLE` → `mark_oracle_spread` (perp
152
+ premium) · `RSI14 value` · `CHG_24H value` → `chg24h`. Plus platform `includeCvd`.
153
+
154
+ **Conditions** (building blocks carry no verdict; NOT as a required veto):
155
+
156
+ ```json
157
+ [
158
+ { "conditionKey": "NO_LIQUIDATION_TAPE", "name": "Not a liquidation knife",
159
+ "definition": { "kind": "group", "op": "NOT", "members": [
160
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "oiRegime" }, "op": "is", "label": "long liquidation" } ] },
161
+ "verdict": null, "required": true },
162
+ { "conditionKey": "CROWDED_LONGS", "name": "Longs overpaying and adding",
163
+ "definition": { "kind": "group", "op": "ALL", "members": [
164
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "ann" }, "op": "gte", "value": 25 },
165
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "oiChg" }, "op": "gte", "value": 3 },
166
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "oiRegime" }, "op": "is", "label": "new longs" } ] },
167
+ "verdict": null, "required": false },
168
+ { "conditionKey": "FADE_SHORT", "name": "Fade the crowded long",
169
+ "definition": { "kind": "group", "op": "ALL", "members": [
170
+ { "kind": "conditionRef", "conditionKey": "CROWDED_LONGS" },
171
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "RSI14" }, "op": "gte", "value": 65 },
172
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "chg24h" }, "op": "gte", "value": 5 } ] },
173
+ "verdict": "DOWN", "required": false },
174
+ { "conditionKey": "SQUEEZED_SHORTS", "name": "Shorts overpaying and adding",
175
+ "definition": { "kind": "group", "op": "ALL", "members": [
176
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "ann" }, "op": "lte", "value": -25 },
177
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "oiChg" }, "op": "gte", "value": 3 },
178
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "oiRegime" }, "op": "is", "label": "new shorts" } ] },
179
+ "verdict": null, "required": false },
180
+ { "conditionKey": "SQUEEZE_LONG", "name": "Squeeze the crowded short",
181
+ "definition": { "kind": "group", "op": "ALL", "members": [
182
+ { "kind": "conditionRef", "conditionKey": "SQUEEZED_SHORTS" },
183
+ { "kind": "clause", "column": { "sectionKey": "custom:b2b2b2b2-c3c3-4d4d-8e5e-f6f6f6f6f6f6", "header": "RSI14" }, "op": "lte", "value": 35 } ] },
184
+ "verdict": "UP", "required": false }
185
+ ]
186
+ ```
187
+
188
+ **Rules** (note the tuned params — read `get_strategy_signal_definition` first):
189
+ `funding_extreme_positive` 3 required, params `{"thresholdPct": 0.001}` ·
190
+ `funding_extreme_negative` 3 required, params `{"thresholdPct": 0.001}` · `oi_surge` 2, params
191
+ `{"thresholdPct": 0.03}` · `rsi_overbought` 2, params `{"threshold": 65}` · `rsi_oversold` 2,
192
+ params `{"threshold": 35}` · `cvd_bear_divergence` 2 · `cvd_bull_divergence` 2 ·
193
+ `mfi_overbought` 1 · `mfi_oversold` 1.
194
+
195
+ **Gates & geometry.** `minAggregateScore 0.6`, `minRequiredCount 1` (either funding extreme),
196
+ `minAtrPct 0.5`. Levels `1.0–2.5` ATR (mean reversion needs room), RR `1.5`. Position
197
+ management: break-even at `0.8R`, trailing off, **timeDecay on** (`grace 120`, `interval 60`,
198
+ `tighten 10`, `max 40`, `stale 25`) — a fade that hasn't paid in a couple of hours is wrong.
199
+
200
+ ---
201
+
202
+ ## 3 · Relative-Strength Rotation with a Benchmark Gate (cross-sectional desk, 4h)
203
+
204
+ **Thesis.** Own the leaders while the market leader trends up; press the laggards when it rolls
205
+ over. Selection is cross-sectional (ranks), regime is read off a **benchmark section**.
206
+
207
+ **Sections.** Custom `Leadership` (key `custom:d4d4…`): `CLOSE_CHANGE rank hi` →
208
+ `closeChg_rank_hi` · `CLOSE_CHANGE rank lo` → `closeChg_rank_lo` · `RVOL rank hi` →
209
+ `RVOL_rank_hi` · `EMA5 spread EMA13 × trajectory w4` → `EMA5_EMA13_spread_now` / `_trend` ·
210
+ `NOTIONAL_VOLUME_1D value` → `vol24hUsd`. Custom `BTC Regime` with `"benchmarkTicker": "BTC"`
211
+ (key `custom:c3c3…`): `MA_ALIGN value` → `MAalign` (bullish/bearish/mixed) · `ADX value` ·
212
+ `REGIME_TREND value` → `regTrend`. Every row of that section reads BTC, not the evaluated coin.
213
+
214
+ **Conditions** (refs compose the regime gate with selection; duplicated headers like `ADX`
215
+ must be section-qualified — that is why the section keys are minted client-side):
216
+
217
+ ```json
218
+ [
219
+ { "conditionKey": "BTC_RISK_ON", "name": "BTC trending up",
220
+ "definition": { "kind": "group", "op": "ALL", "members": [
221
+ { "kind": "clause", "column": { "sectionKey": "custom:c3c3c3c3-d4d4-4e5e-8f6f-a7a7a7a7a7a7", "header": "MAalign" }, "op": "is", "label": "bullish" },
222
+ { "kind": "clause", "column": { "sectionKey": "custom:c3c3c3c3-d4d4-4e5e-8f6f-a7a7a7a7a7a7", "header": "ADX" }, "op": "gte", "value": 20 } ] },
223
+ "verdict": null, "required": false },
224
+ { "conditionKey": "BTC_RISK_OFF", "name": "BTC rolling over",
225
+ "definition": { "kind": "clause", "column": { "sectionKey": "custom:c3c3c3c3-d4d4-4e5e-8f6f-a7a7a7a7a7a7", "header": "MAalign" }, "op": "is", "label": "bearish" },
226
+ "verdict": null, "required": false },
227
+ { "conditionKey": "LEADER", "name": "Top-5 leader with participation",
228
+ "definition": { "kind": "group", "op": "ALL", "members": [
229
+ { "kind": "clause", "column": { "sectionKey": "custom:d4d4d4d4-e5e5-4f6f-8a7a-b8b8b8b8b8b8", "header": "closeChg_rank_hi" }, "op": "lte", "value": 5 },
230
+ { "kind": "clause", "column": { "sectionKey": "custom:d4d4d4d4-e5e5-4f6f-8a7a-b8b8b8b8b8b8", "header": "RVOL_rank_hi" }, "op": "lte", "value": 10 },
231
+ { "kind": "clause", "column": { "sectionKey": "custom:d4d4d4d4-e5e5-4f6f-8a7a-b8b8b8b8b8b8", "header": "vol24hUsd" }, "op": "gte", "value": 100000000 } ] },
232
+ "verdict": null, "required": false },
233
+ { "conditionKey": "ROTATE_IN", "name": "Rotate into leadership",
234
+ "definition": { "kind": "group", "op": "ALL", "members": [
235
+ { "kind": "conditionRef", "conditionKey": "BTC_RISK_ON" },
236
+ { "kind": "conditionRef", "conditionKey": "LEADER" } ] },
237
+ "verdict": "UP", "required": false },
238
+ { "conditionKey": "ROTATE_OUT", "name": "Press laggards in risk-off",
239
+ "definition": { "kind": "group", "op": "ALL", "members": [
240
+ { "kind": "conditionRef", "conditionKey": "BTC_RISK_OFF" },
241
+ { "kind": "clause", "column": { "sectionKey": "custom:d4d4d4d4-e5e5-4f6f-8a7a-b8b8b8b8b8b8", "header": "closeChg_rank_lo" }, "op": "lte", "value": 5 } ] },
242
+ "verdict": "DOWN", "required": false }
243
+ ]
244
+ ```
245
+
246
+ **Rules.** `comparison_sector_momentum` 3 · `rel_roc_positive` 2 · `rel_roc_negative` 2 ·
247
+ `ma_ema_aligned_bull` 2 required · `ma_ema_aligned_bear` 2 required · `rel_ppo_bull_cross` 1 ·
248
+ `rel_ppo_bear_cross` 1 · `volume_surge` 1. Gates: `0.5 / 1 / 0.6`. Levels `1–2` ATR, RR `1.8`.
249
+ PM: break-even 1R; trailing at 1R with giveback 45 (let rotation winners run); no timeDecay.
250
+ `coinSelection` `{"mode":"ranked","limit":40,"category":"CRYPTO"}` — ranks need a wide cohort.
251
+
252
+ ---
253
+
254
+ ## 4 · HTF Trend Pullback (CTA desk, 1h, multi-timeframe confluence)
255
+
256
+ **Thesis.** Only trade with the higher-timeframe trend; enter on anchor-rung pullbacks to value
257
+ while the lower rung has not broken structure. Never spend an LLM call in chop.
258
+
259
+ **Sections.** Platform `includeMtfConfluence` (headers per rung: `MAalign_ltf` / `MAalign` /
260
+ `MAalign_htf`, `RSI14_*_zone`, `ADX_*_state` with vocab weak/developing/trending/extreme),
261
+ platform `includeMovingAverages` (`dist_SMA20`, `dist_EMA20`, …), platform `includeRsi`
262
+ (`RSI14_now`, `RSI14_zone`). Platform section keys are their literal `sectionKey` — qualify
263
+ clauses with e.g. `"sectionKey": "includeMtfConfluence"` where headers collide.
264
+
265
+ **Conditions** — the `ANY`-of-trends required gate is the key move: chop (neither trend) blocks
266
+ compose-trade entirely, before billing:
267
+
268
+ ```json
269
+ [
270
+ { "conditionKey": "HTF_UP", "name": "HTF uptrend",
271
+ "definition": { "kind": "group", "op": "ALL", "members": [
272
+ { "kind": "clause", "column": { "sectionKey": "includeMtfConfluence", "header": "MAalign_htf" }, "op": "is", "label": "bullish" },
273
+ { "kind": "clause", "column": { "sectionKey": "includeMtfConfluence", "header": "ADX_htf_state" }, "op": "in", "labels": ["trending", "extreme"] } ] },
274
+ "verdict": null, "required": false },
275
+ { "conditionKey": "HTF_DOWN", "name": "HTF downtrend",
276
+ "definition": { "kind": "group", "op": "ALL", "members": [
277
+ { "kind": "clause", "column": { "sectionKey": "includeMtfConfluence", "header": "MAalign_htf" }, "op": "is", "label": "bearish" },
278
+ { "kind": "clause", "column": { "sectionKey": "includeMtfConfluence", "header": "ADX_htf_state" }, "op": "in", "labels": ["trending", "extreme"] } ] },
279
+ "verdict": null, "required": false },
280
+ { "conditionKey": "TREND_PRESENT", "name": "Some HTF trend exists",
281
+ "definition": { "kind": "group", "op": "ANY", "members": [
282
+ { "kind": "conditionRef", "conditionKey": "HTF_UP" },
283
+ { "kind": "conditionRef", "conditionKey": "HTF_DOWN" } ] },
284
+ "verdict": null, "required": true },
285
+ { "conditionKey": "PULLBACK_LONG", "name": "Pullback to value in uptrend",
286
+ "definition": { "kind": "group", "op": "ALL", "members": [
287
+ { "kind": "conditionRef", "conditionKey": "HTF_UP" },
288
+ { "kind": "clause", "column": { "sectionKey": "includeRsi", "header": "RSI14_now" }, "op": "between", "low": 35, "high": 55 },
289
+ { "kind": "clause", "column": { "sectionKey": "includeMovingAverages", "header": "dist_EMA20" }, "op": "between", "low": -3, "high": 0.5 } ] },
290
+ "verdict": "UP", "required": false },
291
+ { "conditionKey": "PULLBACK_SHORT", "name": "Rally to value in downtrend",
292
+ "definition": { "kind": "group", "op": "ALL", "members": [
293
+ { "kind": "conditionRef", "conditionKey": "HTF_DOWN" },
294
+ { "kind": "clause", "column": { "sectionKey": "includeRsi", "header": "RSI14_now" }, "op": "between", "low": 45, "high": 65 },
295
+ { "kind": "clause", "column": { "sectionKey": "includeMovingAverages", "header": "dist_EMA20" }, "op": "between", "low": -0.5, "high": 3 } ] },
296
+ "verdict": "DOWN", "required": false }
297
+ ]
298
+ ```
299
+
300
+ **Rules.** `mtf_pullback_long` 3 required · `mtf_pullback_short` 3 required ·
301
+ `htf_ma_aligned_bull` 2 required · `htf_ma_aligned_bear` 2 required · `htf_trend_adx_trending`
302
+ 2 · `ma_ema_aligned_bull` 1 · `ma_ema_aligned_bear` 1 · `rsi_oversold` 1 params
303
+ `{"threshold": 40}` (pullback depth, not capitulation). Gates: `minAggregateScore 0.6`,
304
+ `minRequiredCount 2` (a pullback signal *and* an HTF alignment), `minAtrPct 0.7`. Levels
305
+ `1–2.5` ATR, RR `2`. PM: break-even 1R; trailing 1.5R giveback 30 buffer 0.3; no timeDecay.
306
+
307
+ ---
308
+
309
+ ## 5 · Perp/Spot Flow Divergence at Structure (microstructure desk, 15m scalp)
310
+
311
+ **Thesis.** When spot flow leads (accumulation) and price sits just above a standing support
312
+ zone, buy the dip; when a move is perp-led and fragile into resistance, fade it. Structure comes
313
+ from the platform zones section; flow from the perp/spot module.
314
+
315
+ **Sections.** Platform `includePerpSpotFlow` (`perpSpotFlow` vocab: `confirmed_bull` /
316
+ `confirmed_bear` / `perp_led_fragile` / `spot_led_accumulation` / `neutral`; plus
317
+ `perpSpotStr`, `spotCVD`), platform `includeStructureZones` (`zones_htf_support_dist` — signed %
318
+ from price, support below price is negative; `zones_htf_resist_dist`, `_age_h`, `_type`),
319
+ custom `Tape` (key `custom:e5e5…`): `BUY_PRESSURE value` → `buyPres` (0–1) · `RVOL value` ·
320
+ `CLOSE_CHANGE value` → `closeChg`.
321
+
322
+ **Conditions** — `N_OF` requires 2-of-3 tape confirmations instead of a brittle ALL:
323
+
324
+ ```json
325
+ [
326
+ { "conditionKey": "SPOT_ACCUM", "name": "Spot-led accumulation",
327
+ "definition": { "kind": "clause", "column": { "sectionKey": "includePerpSpotFlow", "header": "perpSpotFlow" }, "op": "is", "label": "spot_led_accumulation" },
328
+ "verdict": null, "required": false },
329
+ { "conditionKey": "NEAR_SUPPORT", "name": "Sitting on a standing zone",
330
+ "definition": { "kind": "group", "op": "ALL", "members": [
331
+ { "kind": "clause", "column": { "sectionKey": "includeStructureZones", "header": "zones_htf_support_dist" }, "op": "between", "low": -2, "high": 0 },
332
+ { "kind": "clause", "column": { "sectionKey": "includeStructureZones", "header": "zones_htf_support_age_h" }, "op": "gte", "value": 12 } ] },
333
+ "verdict": null, "required": false },
334
+ { "conditionKey": "DIP_BID", "name": "Buy the dip into support",
335
+ "definition": { "kind": "group", "op": "ALL", "members": [
336
+ { "kind": "conditionRef", "conditionKey": "SPOT_ACCUM" },
337
+ { "kind": "conditionRef", "conditionKey": "NEAR_SUPPORT" },
338
+ { "kind": "group", "op": "N_OF", "n": 2, "members": [
339
+ { "kind": "clause", "column": { "sectionKey": "custom:e5e5e5e5-f6f6-4a7a-8b8b-c9c9c9c9c9c9", "header": "buyPres" }, "op": "gte", "value": 0.55 },
340
+ { "kind": "clause", "column": { "sectionKey": "custom:e5e5e5e5-f6f6-4a7a-8b8b-c9c9c9c9c9c9", "header": "RVOL" }, "op": "gte", "value": 1.2 },
341
+ { "kind": "clause", "column": { "sectionKey": "custom:e5e5e5e5-f6f6-4a7a-8b8b-c9c9c9c9c9c9", "header": "closeChg" }, "op": "gt", "value": 0 } ] } ] },
342
+ "verdict": "UP", "required": false },
343
+ { "conditionKey": "FRAGILE_POP", "name": "Perp-led pop into resistance",
344
+ "definition": { "kind": "group", "op": "ALL", "members": [
345
+ { "kind": "clause", "column": { "sectionKey": "includePerpSpotFlow", "header": "perpSpotFlow" }, "op": "is", "label": "perp_led_fragile" },
346
+ { "kind": "clause", "column": { "sectionKey": "includeStructureZones", "header": "zones_htf_resist_dist" }, "op": "between", "low": 0, "high": 2 },
347
+ { "kind": "clause", "column": { "sectionKey": "custom:e5e5e5e5-f6f6-4a7a-8b8b-c9c9c9c9c9c9", "header": "RVOL" }, "op": "gte", "value": 1.3 } ] },
348
+ "verdict": "DOWN", "required": false }
349
+ ]
350
+ ```
351
+
352
+ **Rules.** `flow_perp_spot_bull_divergence` 3 required · `flow_perp_spot_bear_divergence` 3
353
+ required · `cvd_bull_divergence` 2 · `cvd_bear_divergence` 2 · `sr_at_support` 2 ·
354
+ `sr_at_resistance` 2 · `structure_ob_approach` 1 · `volume_surge` 1. Gates: `0.55 / 1 / 0.4`
355
+ (15m: lower ATR floor). Levels `0.5–1.2` ATR, RR `1.5` — scalp geometry. PM: break-even
356
+ `0.7R`; trailing `0.9R`, giveback 30, buffer 0.15; **timeDecay on and aggressive** (`grace 45`,
357
+ `interval 15`, `tighten 15`, `max 60`, `stale 30`). `coinSelection`
358
+ `{"mode":"ranked","limit":15,"category":"CRYPTO"}`.
359
+
360
+ ---
361
+
362
+ ## Using a playbook
363
+
364
+ 1. Re-discover the vocabulary; confirm each header via `get_strategy_column_contract` or one
365
+ `preview_strategy_report` over your sections (read `conditionColumns`).
366
+ 2. Adapt thresholds to the current market — preview shows live values beside each clause, so
367
+ calibrate literals against what the table actually renders.
368
+ 3. Compile once, read `approvedPlan` (scorecard, diff, viability) and
369
+ `reviewContext.reportPreview` (condition outcomes, verdict tally, budgets), fix, recompile.
370
+ 4. Apply with `{ "request": { "planToken": "<verbatim>", "confirm": true } }` after review.
@@ -0,0 +1,129 @@
1
+ # Strategy Studio recipes — copy-adaptable fragments
2
+
3
+ Validated against the live server 2026-08-28; shapes binding, tokens illustrative — re-discover
4
+ before compiling. Column recipes are objects for a custom section's `columns[]`; condition
5
+ recipes drop into `conditions[]`.
6
+
7
+ ## Column recipes
8
+
9
+ | Intent | Column | Header it generates |
10
+ |---|---|---|
11
+ | Live traded price | `{ "metric": "LAST", "transformId": "value", "timeframe": { "rel": "anchor" } }` | `last` |
12
+ | Momentum build-up | `{ "metric": "RSI14", "transformId": "trajectory", "timeframe": { "rel": "anchor" }, "window": 4 }` | `RSI14_t3…_now`, `RSI14_trend` |
13
+ | Closed-bar-only trajectory (no forming-bar noise) | add `"bars": "closed"` to any trajectory | same, drawn from closed bars |
14
+ | Extension from trend | `{ "metric": "SMA50", "transformId": "distance", "timeframe": { "rel": "anchor" } }` | `dist_SMA50` (signed %) |
15
+ | Perp premium | `{ "metric": "MARK", "transformId": "spread", "timeframe": { "rel": "anchor" }, "inputs": [{ "metric": "ORACLE" }] }` | `mark_oracle_spread` |
16
+ | Cross-venue basis | `MARK spread SPOT_CLOSE_BN` (or `SPOT_CLOSE_CB`) | `mark_bnClose_spread` |
17
+ | Funding paid over a day | `{ "metric": "FUNDING_RATE", "transformId": "aggregate", "timeframe": { "rel": "anchor" }, "window": 24 }` | `rate_mean24` |
18
+ | Board-wide standing | `{ "metric": "ATR_PCT", "transformId": "rank", "timeframe": { "rel": "anchor" }, "ordering": "hi" }` | `atrPct_rank_hi` (1 = most volatile) |
19
+ | Biggest movers, sign-agnostic | `CLOSE_CHANGE rank far` | `closeChg_rank_far` |
20
+ | Most extended from VWAP on the board | `{ "metric": "VWAP", "transformId": "distance", "chainedTransformId": "rank", "timeframe": { "rel": "anchor" }, "ordering": "far" }` | `dist_VWAP_rank_far` (confirm via contract) |
21
+ | EMA ribbon compression trend | `{ "metric": "EMA5", "transformId": "spread", "chainedTransformId": "trajectory", "timeframe": { "rel": "anchor" }, "inputs": [{ "metric": "EMA13" }], "window": 4 }` | `EMA5_EMA13_spread_now`, `_trend` |
22
+ | Chop filter on the entry rung | `{ "metric": "CLOSE", "transformId": "efficiency", "timeframe": { "rel": "lower" }, "window": 5, "bars": "closed" }` | `close_ltf_er` (≥0.6 = directional) |
23
+ | One-bar volume concentration | `{ "metric": "VOLUME", "transformId": "maxShare", "timeframe": { "rel": "lower" }, "window": 4, "bars": "closed" }` | `volBase_ltf_maxShare` |
24
+ | Room to structure | `{ "metric": "STRUCT_ZONES", "transformId": "nearestZoneDist", "timeframe": { "rel": "regime" }, "side": "resistance" }` | `zones_htf_resist_dist` |
25
+ | Crowd fade input | `{ "metric": "CROWD_UPBIAS", "transformId": "value", "timeframe": { "rel": "anchor" } }` | `upBias` (%, last 4 settled sessions) |
26
+ | Daily trend state on any anchor | `{ "metric": "MA_ALIGN", "transformId": "value", "timeframe": { "abs": "1d" } }` | `MAalign_1d` |
27
+ | Last **closed** daily read (deterministic) | `{ "metric": "RSI14", "transformId": "value", "timeframe": { "abs": "1d" }, "offset": 1 }` | `RSI14_1d` — formula `RSI14[t - 1]` |
28
+ | Daily extension from the 200 | `{ "metric": "SMA200", "transformId": "distance", "timeframe": { "abs": "1d" } }` | `dist_SMA200_1d` |
29
+ | Daily-structure breakout state | `{ "metric": "PRICE_ZONE", "transformId": "value", "timeframe": { "abs": "1d" } }` | `zone_1d` — `is "breakout high"` / `"breakdown low"` |
30
+ | Room to the daily swing high | `{ "metric": "SWING_HIGH", "transformId": "distance", "timeframe": { "abs": "1d" } }` | `dist_swingHi_1d` (signed %; `gte 0` = above it) |
31
+
32
+ Rung suffixes: `{ "rel": "lower" }` → `_ltf`, `{ "rel": "regime" }` → `_htf` (the anchor's
33
+ ladder successor — 1d for a 4h anchor), pinned `{ "abs": "<tf>" }` → `_<tf>` (e.g. `_1d`).
34
+ Pins bind to discovery's `rankedTimeframes` (a superset of the anchor set), are fixed across
35
+ anchor retunes, and `offset` does not change the header — one offset per `metric × timeframe`
36
+ per section. `offset` is a `value`-transform parameter only: `distance` rejects it (typed
37
+ error), so a level is always measured as it stands now — the literal *previous-day* high/low
38
+ (PDH/PDL) is not addressable, and the daily swing levels (`zone_1d`, `dist_swingHi_1d`) are
39
+ the named nearest level for daily-breakout intents. Conditions compare a column against a
40
+ literal, never against another column. Rank ordering semantics: `hi` most positive, `lo` most
41
+ negative, `far`/`near` by magnitude (offered only where sign matters).
42
+
43
+ ## Condition patterns
44
+
45
+ **Band (between).** RSI pullback zone rather than a single threshold:
46
+ `{ "kind": "clause", "column": { "sectionKey": null, "header": "RSI14_now" }, "op": "between", "low": 35, "high": 55 }`
47
+ (`sectionKey: null` is legal only while the header is unique report-wide.)
48
+
49
+ **Label membership (in).** Accept two of a classification's states:
50
+ `{ "kind": "clause", "column": { "sectionKey": "includeMtfConfluence", "header": "ADX_htf_state" }, "op": "in", "labels": ["trending", "extreme"] }`
51
+
52
+ **Veto (NOT + required).** Block the whole compose-trade path on a disqualifier — evaluated
53
+ before any billing or LLM call:
54
+
55
+ ```json
56
+ { "conditionKey": "NO_EVENT_RISK", "name": "No liquidation tape", "verdict": null, "required": true,
57
+ "definition": { "kind": "group", "op": "NOT", "members": [
58
+ { "kind": "clause", "column": { "sectionKey": null, "header": "oiRegime" }, "op": "is", "label": "long liquidation" } ] } }
59
+ ```
60
+
61
+ **Quorum (N_OF).** Robust confirmation instead of a brittle ALL:
62
+
63
+ ```json
64
+ { "kind": "group", "op": "N_OF", "n": 2, "members": [
65
+ { "kind": "clause", "column": { "sectionKey": null, "header": "buyPres" }, "op": "gte", "value": 0.55 },
66
+ { "kind": "clause", "column": { "sectionKey": null, "header": "RVOL" }, "op": "gte", "value": 1.2 },
67
+ { "kind": "clause", "column": { "sectionKey": null, "header": "closeChg" }, "op": "gt", "value": 0 } ] }
68
+ ```
69
+
70
+ **Building block + carriers.** Factor shared state once (`verdict: null`), reference it from
71
+ directional carriers; the first TRUE carrier **in declaration order** decides — order carriers
72
+ most-specific first.
73
+
74
+ **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.
76
+
77
+ **Required-count trap.** `required: true` at `allocation: 0` on a *signal rule* is rejected
78
+ (contract 34). A required *condition* is independent of signal weights — the two gates stack.
79
+
80
+ ## Weight matrices (rules)
81
+
82
+ **Conviction pyramid (default).** 1–2 × Critical (the thesis signals, usually `required`),
83
+ 2–4 × Important (independent confirmation — different modules, not correlated oscillators),
84
+ 1–3 × Normal (context), everything else Off. With weights 3/2/2/1 and the Critical + one
85
+ Important triggered at score 1.0: aggregate = (3+2)/(3+2+2+1) = 0.625 → a gate of 0.6 means
86
+ "thesis + one confirmation, minimum".
87
+
88
+ **Gate math check.** Before setting `minAggregateScore`, list the trigger combinations that
89
+ should route, compute Σ(score×alloc)/Σ(alloc) for the weakest acceptable one, and gate just
90
+ below it. `simulate_aggregate_score` does this arithmetic for you from compiled values.
91
+
92
+ **Directional symmetry.** A both-ways book weights bull/bear siblings identically
93
+ (`ma_ema_aligned_bull`/`_bear` both 2-required) and lets conditions decide direction.
94
+
95
+ **Param tuning.** Always read `get_strategy_signal_definition({ signalId, timeframe })` first;
96
+ send the full replacement object. Examples validated as canonical defaults: `volume_surge`
97
+ `{"multiplier": 2}`, `rsi_overbought` `{"threshold": 70}`, `funding_extreme_positive`
98
+ `{"thresholdPct": 0.0005}`, `trend_adx_trending` `{"threshold": 25}`,
99
+ `comparison_btc_decorrelation` `{"maxCorrelation": 0.3}`.
100
+
101
+ ## Trade-level + position-management presets by persona
102
+
103
+ | Persona (anchor) | Stop band (ATR) | RR | Break-even | Trailing | Time decay |
104
+ |---|---|---|---|---|---|
105
+ | Scalper (5m/15m) | 0.5 – 1.2 | 1.5 | 0.7R | 0.9R, giveback 30, buffer 0.15 | ON: 45/15 min, tighten 15 → max 60, stale 30 |
106
+ | Intraday mean-revert (1h) | 1.0 – 2.5 | 1.5 | 0.8R | off | ON: 120/60 min, tighten 10 → max 40, stale 25 |
107
+ | Swing breakout (4h) | 0.75 – 1.75 | 2 | 1R | 1.2R, giveback 35, buffer 0.3 | off |
108
+ | Swing trend (4h) | 1.0 – 2.5 | 2 | 1R | 1.5R, giveback 45–55, buffer 0.3 | off |
109
+
110
+ Bounds to respect (validated): break-even trigger 0.5–2R; trailing trigger 0–2R step 0.01
111
+ (0 = trail from entry); giveback 25–55%; buffer 0.01–1%; grace ≥ interval; stop-band floor <
112
+ ceiling ≤ the 3×ATR structural cap; RR within the catalog's served range (0.5–3 today). Wider
113
+ stop ⇒ smaller position (risk-budget sizing), never more risk.
114
+
115
+ ## Coin selection patterns
116
+
117
+ - Focused edit / review: `{ "mode": "explicit", "tickers": ["BTC", "ETH", "SOL"] }`
118
+ - Scanning book: `{ "mode": "ranked", "limit": 40, "category": "CRYPTO" }`
119
+ - Sector books: `category` from the served list (`L1`, `MEMES`, `DEFI`, `TRADFI`, `STOCKS`,
120
+ `INDICES`, `COMMODITIES`) — e.g. a memes-only fade book previews against `MEMES`.
121
+ - Rank-based conditions need a cohort at least as wide as the rank thresholds they test.
122
+
123
+ ## Market Read patterns
124
+
125
+ - Reference the decision, not just data: `"Act only while {SQUEEZE_ON} is TRUE"` renders the
126
+ condition's outcome *with its evidence*.
127
+ - Qualify on collision: `{custom:<uuid>.MAalign}` / `{includeMtfConfluence.MAalign}`.
128
+ - After previewing, check `marketReadMarkers[].status` — fix every `unknown` / `ambiguous`
129
+ marker before compiling; `qualifiedForms` lists the exact replacements.
@@ -0,0 +1,214 @@
1
+ # TradingView ports — popular scripts, translated to the studio
2
+
3
+ The most-used TradingView community scripts encode processes traders already trust. This file
4
+ ports the **process** of each — regime filter → setup state → trigger → stop/exit engine — onto
5
+ the studio's validated vocabulary. It never claims formula parity: where an indicator does not
6
+ exist in the catalog, the substitution is named (the studio's rule: offer the nearest
7
+ expressible thing **labelled as a substitute**, never as the thing itself). Headers below were
8
+ validated live on 2026-08-28; re-discover before compiling.
9
+
10
+ **Event columns caveat (read first).** `crossDetect` and event metrics (`MACD_cross`,
11
+ `EMA5_13`) print `Bullish`/`Bearish` only on the bar a cross occurs and are null otherwise —
12
+ null resolves a clause UNRESOLVED, not FALSE. Use them as *triggers* inside a carrier, and pair
13
+ them with a persistent *state* (the spread's sign, `MAalign`) for regime, exactly as the source
14
+ scripts separate trigger from filter.
15
+
16
+ ## 0 · Daily-chart strategies — the pattern that carries every TV daily script
17
+
18
+ Most TradingView strategies run on the daily chart. The studio's anchors top out intraday
19
+ (discovery's `timeframes`), but **pinned timeframes** carry the daily thesis onto any anchor:
20
+ `{abs: "1d"}` columns are valid wherever discovery's `rankedTimeframes` includes `1d`
21
+ (validated), render with a `_1d` header suffix (`MAalign_1d`, `dist_SMA200_1d`, `RSI14_1d`),
22
+ and ignore anchor retunes. Two dials make the port faithful:
23
+
24
+ - **`offset: 1` on a pinned `value` column reads the last CLOSED daily bar** (`RSI14[t - 1]`,
25
+ validated) — the strategy's daily inputs then change exactly once per daily close, so a
26
+ required condition on them decides "on daily closes" even though the anchor cycle re-checks
27
+ intraday. Offset 0 reads the forming daily bar (marked provisional).
28
+ - **A 4h anchor's regime rung is the daily** (ladder successor), so `{rel: "regime"}` columns
29
+ (`MAalign_htf`) and the `htf_*` signals already evaluate on 1d for 4h strategies.
30
+
31
+ Recipe: anchor 4h → pinned-1d thesis section (trend, RSI, distance-to-MA at `offset: 1`) →
32
+ `required: true` daily conditions gating every carrier → anchor-rung columns only for entry
33
+ timing and risk. The result is arguably stronger than the TV original: decisions bind to daily
34
+ closes while stops, trailing, and time decay keep managing the position intraday.
35
+
36
+ ## Expressibility triage
37
+
38
+ | TradingView script | Verdict | Port anchor |
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 |
42
+ | Chandelier Exit | Direct port of the *mechanism* | trailing from entry (`trailingTriggerR: 0`) |
43
+ | MACD + 200 MA filter | Direct process port | `MACD_cross`, `dist_SMA200`, `macd_bull_cross` |
44
+ | Golden / Death Cross | Direct process port | `SMA50_SMA200_spread` + `_trend` |
45
+ | RSI-2 (Connors) | Named substitute (no RSI-2; RSI7 is the closest series) | `RSI7`, `dist_SMA200`, time decay |
46
+ | VWAP reversion | Direct process port | `dist_VWAP`, `dist_VWAP_rank_far` |
47
+ | Donchian / Turtle breakout | Process port (N-bar channel → swing structure) | `zone`, `dist_swingHi`, `sr_resistance_break` |
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 | — |
50
+
51
+ ---
52
+
53
+ ## 1 · Squeeze Momentum [LazyBear] (TTM Squeeze) — 4h
54
+
55
+ **Source process.** Squeeze ON while Bollinger Bands sit inside Keltner Channels (volatility
56
+ coiled); wait; when the squeeze releases, enter in the direction of the momentum histogram.
57
+
58
+ **Port.** Keltner Channels are not in the catalog, so squeeze detection substitutes
59
+ *cross-sectional and absolute* Bollinger compression — a stricter, universe-aware read:
60
+ `bbWidthPct_rank_lo lte 10` AND `ADX lt 20` as the `SQUEEZE_ON` building block. Momentum
61
+ direction comes from the MACD histogram trajectory (`MACD_now gt 0` with `MACD_trend rising`
62
+ for longs) instead of the LazyBear linreg histogram. Release = expansion bar with
63
+ participation: `RVOL gte 1.5` and `pctB` beyond 0.85 / 0.15. This is Playbook 1
64
+ (`playbooks.md`) — it *is* the LazyBear process under studio vocabulary; keep
65
+ `bollinger_squeeze` at Critical-required (its `bandwidthPct` param is the absolute half of the
66
+ detection) and `volatility_atr_expanding` as the release confirmation.
67
+
68
+ ## 2 · Supertrend / UT Bot Alerts — 1h/4h
69
+
70
+ **Source process.** An ATR-offset trailing line flips below/above price; entries on the flip,
71
+ and the line itself is the stop for the life of the trade.
72
+
73
+ **Port — the key insight: the studio's stop engine is native, not an indicator.** What the
74
+ Pine script simulates with a plotted line, position management executes: enable trailing with
75
+ `trailingTriggerR: 0` (trail from entry — the Supertrend/UT Bot behavior), `giveback` as the
76
+ ATR-offset analog (30–40 tight like factor-2 Supertrend, 45–55 loose like factor-3), plus the
77
+ 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:
79
+ `MAalign is "bullish"` (state) with the `EMA5_13 is "Bullish"` cross event as the trigger, and
80
+ `ma_ema_bull_cross` / `ma_ema_aligned_bull` Critical/Important-required in `rules`. Say the
81
+ substitution out loud when presenting the strategy.
82
+
83
+ ```json
84
+ "trailingEnabled": true, "trailingTriggerR": 0, "trailingGivebackPct": 35,
85
+ "trailingBufferPct": 0.2, "breakEvenEnabled": false
86
+ ```
87
+
88
+ ## 3 · Chandelier Exit — any persona
89
+
90
+ **Source process.** Stop trails the highest high since entry minus `3 × ATR(22)`; never widens.
91
+
92
+ **Port.** Pure position-management mapping — trail from entry with a giveback sized to the
93
+ chandelier distance: `trailingTriggerR: 0`, `trailingGivebackPct` ~45–55 (a 3×ATR pullback on
94
+ a typical setup surrenders roughly half its open run — tune against the previewed `atrPct`),
95
+ `maxStopLossAtrMultiple` near the structural cap so the initial stop can sit chandelier-wide.
96
+ The studio stop also never widens; time decay stays off (chandelier gives trends room).
97
+
98
+ ## 4 · MACD + 200 MA filter — 4h (the classic trend-following combo)
99
+
100
+ **Source process.** Only long while price is above the 200-period average; enter on a MACD
101
+ bullish cross; mirror for shorts below.
102
+
103
+ **Port.** Regime as a required building block, cross as the carrier's trigger:
104
+
105
+ ```json
106
+ [
107
+ { "conditionKey": "ABOVE_200", "name": "Above the 200 SMA",
108
+ "definition": { "kind": "clause", "column": { "sectionKey": null, "header": "dist_SMA200" }, "op": "gt", "value": 0 },
109
+ "verdict": null, "required": false },
110
+ { "conditionKey": "MACD_LONG", "name": "MACD bull cross in uptrend",
111
+ "definition": { "kind": "group", "op": "ALL", "members": [
112
+ { "kind": "conditionRef", "conditionKey": "ABOVE_200" },
113
+ { "kind": "clause", "column": { "sectionKey": null, "header": "MACD_cross" }, "op": "is", "label": "Bullish" } ] },
114
+ "verdict": "UP", "required": false }
115
+ ]
116
+ ```
117
+
118
+ Columns: `SMA200 distance` → `dist_SMA200`; `MACD crossDetect` → `MACD_cross` (event — TRUE
119
+ only on the crossing bar, otherwise UNRESOLVED; that momentariness is faithful to the source,
120
+ which also only fires on the cross). Rules: `macd_bull_cross`/`macd_bear_cross` 3 required ·
121
+ `ma_sma200_above`/`ma_sma200_below` 2 required · `trend_adx_trending` 1. Gates ~0.6 / 2.
122
+ Swing-trend geometry (1–2.5 ATR, RR 2, trail late).
123
+
124
+ ## 5 · Golden Cross / Death Cross regime book — 4h position
125
+
126
+ **Source process.** SMA50 crossing above SMA200 defines the bull regime; below, the bear.
127
+
128
+ **Port.** The spread composition gives both the *state* (sign) and the *freshness* (trend):
129
+ `SMA50 spread SMA200 × trajectory w4` → `SMA50_SMA200_spread_now` (>0 = golden regime) and
130
+ `SMA50_SMA200_spread_trend` (`rising` = the cross is developing, not decaying). Carrier:
131
+ `GOLDEN` verdict UP = `spread_now gt 0` AND `trend is "rising"`; add a market-breadth gate —
132
+ the always-addressable `mktBreadth_crypto gte 0` (net % of the crypto field closing up) keeps
133
+ the book out of single-name traps. Rules: `ma_sma200_above` 3 required, `ma_ema_aligned_bull`
134
+ 2, `htf_ma_aligned_bull` 2. Position persona: wide stops (1.5–2.5 ATR), RR 2, trail
135
+ 1.5R/giveback 50.
136
+
137
+ ## 6 · RSI-2 (Larry Connors) — 1h mean reversion
138
+
139
+ **Source process.** In an uptrend (close > 200 SMA), buy panic dips (RSI(2) < 10); exit fast
140
+ (cross of the 5-period average / a few bars).
141
+
142
+ **Port — named substitute.** The catalog carries no RSI(2); `RSI7` is the fastest RSI series
143
+ and `lte 10` on it is a rarer, deeper panic — state that trade-off rather than hiding it.
144
+ `DIP_BUY` verdict UP = `dist_SMA200 gt 0` (ref a required `ABOVE_200`) AND `RSI7 lte 10`.
145
+ Rules: `rsi_oversold` 3 required `{"threshold": 25}` (RSI14 gate tuned toward the fast-dip
146
+ regime — read the signal schema first) · `ma_sma200_above` 2 required · `bollinger_lower_touch`
147
+ 1. The source's fast exit is time, not price: `timeDecayEnabled: true`, grace 180, interval
148
+ 60, tighten 15 → max 50, stale 20 — the studio's honest port of "if it hasn't bounced in a few
149
+ bars, leave". RR 1.2–1.5, stops 1.5–2.5 ATR (dips need room), break-even 0.8R.
150
+
151
+ ## 7 · VWAP reversion — 15m/1h intraday
152
+
153
+ **Source process.** Fade stretches away from session VWAP back toward it; stand aside when the
154
+ stretch is trend, not noise.
155
+
156
+ **Port.** `VWAP distance` → `dist_VWAP` (signed %); the board-relative stretch is
157
+ `VWAP distance × rank far` → `dist_VWAP_rank_far lte 5` (top-5 most stretched either way).
158
+ `FADE_TO_VWAP` verdict from the sign: DOWN when `dist_VWAP gte 2` and `RSI14 gte 65`; UP when
159
+ `dist_VWAP lte -2` and `RSI14 lte 35`; veto trend days with a required
160
+ `NOT [ADX_state in ["trending","extreme"]]`. Rules: `rsi_overbought`/`rsi_oversold` 2 each,
161
+ `bollinger_upper_touch`/`lower_touch` 2, `cvd_bear_divergence`/`bull_divergence` 1. Scalp
162
+ geometry + aggressive time decay (VWAP reversion is a session trade — the anchor resets daily
163
+ at 00:00 UTC, which the column's own gloss states).
164
+
165
+ ## 8 · Donchian / Turtle breakout — 4h
166
+
167
+ **Source process.** Buy the N-bar-high breakout, ride with a wide trailing stop, exit on the
168
+ opposite channel.
169
+
170
+ **Port.** The catalog's structural highs replace the fixed N-bar channel: `zone is
171
+ "breakout high"` (price beyond the tracked swing high) with `dist_swingHi gte 0` as the
172
+ numeric confirmation, `RVOL gte 1.5` for participation, and `sr_resistance_break` 3 required
173
+ in rules (its own params define the break). Turtle exits are the trend preset: stops 1.5–2.5
174
+ ATR, RR 2+, trail from 1R with giveback 50, no time decay. Mirror with `"breakdown low"` /
175
+ `dist_swingLo` / `sr_support_break` for shorts. **Daily variant on any anchor:** pin the same
176
+ structure at 1d — `zone_1d is "breakout high"`, `dist_swingHi_1d gte 0` (both validated) —
177
+ which is the studio's daily-breakout condition. Note the substitution: the level is the
178
+ tracked *daily swing* pivot, not the literal previous-day high — `distance` takes no `offset`
179
+ and clauses never compare two columns, so PDH/PDL as exact levels are not addressable (see
180
+ the not-expressible table).
181
+
182
+ ## 9 · ICT / Smart Money Concepts: FVG + Order Blocks — 15m/1h
183
+
184
+ **Source process.** Establish higher-timeframe bias; wait for price to return into a fair
185
+ value gap or order block aligned with that bias; enter on the reaction, stop beyond the zone.
186
+
187
+ **Port — the zone half is native.** `STRUCT_ZONES` *is* the platform's FVG/order-block engine:
188
+ `zones_htf_support_type` (`bullish FVG` / `bullish order block`), `zones_htf_support_dist`
189
+ (signed %; support below price is negative — `between -1.5 0` = sitting on the zone),
190
+ `zones_htf_support_age_h gte 12` (a standing zone, not the last move's shadow). Bias =
191
+ `MAalign_htf is "bullish"` as a required building block. Carrier: bias + in-zone + a tape
192
+ quorum (`N_OF(2)`: `buyPres gte 0.55`, `RVOL gte 1.2`, `closeChg gt 0`). Rules:
193
+ `structure_fvg_approach` / `structure_ob_approach` 3/2 required (their `proximityPct` params
194
+ are the "in the zone" dial), `structure_zone_confluence` 2, `sr_at_support` 2. Stops: tight
195
+ 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,
197
+ killzone clocks, and Turtle-Soup false-break sequencing (ordering between events is outside
198
+ the grammar) — offer this zone-reaction port as the nearest expressible neighbor, labelled.
199
+
200
+ ## Not expressible — and the honest nearest neighbor
201
+
202
+ | Script | Missing primitive | Nearest expressible neighbor |
203
+ |---|---|---|
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` |
212
+
213
+ Never compile a "port" of these under the source's name without the substitution note — a
214
+ player who asked for WaveTrend and silently got Stochastic has no way to learn otherwise.