@battlegrid/mcp-server 31.2.87 → 31.2.88

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/dist/index.d.ts CHANGED
@@ -49,7 +49,7 @@ import { type Implementation, type Prompt, type Resource } from '@modelcontextpr
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.2.87";
52
+ export declare const PACKAGE_VERSION = "31.2.88";
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.2.87';
55
+ export const PACKAGE_VERSION = '31.2.88';
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.2.87",
3
+ "version": "31.2.88",
4
4
  "description": "BattleGrid MCP server — play crypto prediction games from AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "generator": "battlegrid-app/server/scripts/export-mcp-skills.mjs",
3
- "contractVersion": "92.0.0",
3
+ "contractVersion": "105.0.0",
4
4
  "files": {
5
- "battlegrid-agent-management/SKILL.md": "e5ffab64e6a369d5c783aed2b1c7c513b01b870063fd859c9cc4d9fc45d30aac",
6
- "battlegrid-arena-play/SKILL.md": "01b2192c19b93b2315ff1a2b512ee0bcb7fd809c7d77fbbd93307fe2f45924a3",
5
+ "battlegrid-agent-management/SKILL.md": "8ebb7d4fcc5d6a9ce463a762bf58d04a7f641369f7feaf64d2489727531e6f00",
6
+ "battlegrid-agent-management/references/agent-stage-examples.md": "d47555906ce8dedf4c17422f8f5d60187f2d6b3c0af5d9545617a10a5bdb59a1",
7
+ "battlegrid-arena-play/SKILL.md": "25f078eb31db5cd3d78ce0bc251802c32f94234fd8a8c6467c5b68abc41706f2",
7
8
  "battlegrid-market-analysis/SKILL.md": "875bda71dd766ad06ac8e508679bcd3400a600cf74d2eac6c922dbe23175f325",
8
- "battlegrid-radar-deployment/SKILL.md": "aaa4d13d32d470582d4bab0dc71594d4adf6b665bf45ee3384ee794dd8f82ba4",
9
- "battlegrid-strategy-authoring/SKILL.md": "d4c5c87d9306193be8be77b1ade4938bbbc8805d66bc5267c00c2a8db156a92e",
9
+ "battlegrid-radar-deployment/SKILL.md": "093ac358cd4f1a67a7dd467b0b4161c9a055cdd516d49ebb23bd4e9ef44aa200",
10
+ "battlegrid-radar-deployment/references/arena-stage-examples.md": "f924b70801a4b72bbde5a80823408939903cba6b294d8554fc4afc11d80b3333",
11
+ "battlegrid-radar-deployment/references/radar-stage-examples.md": "1ddad4a11f021dd65234ee65cc12d7d1561fe9acfd61cb021f4352080d4cecd5",
12
+ "battlegrid-strategy-authoring/SKILL.md": "fcc4ab6ae7e75e0949cef057134288e5e6e67ff96d3238bb551775ba5ce22f95",
13
+ "battlegrid-strategy-authoring/references/strategy-stage-examples.md": "1c35353c7280934486b65d5defe80f87278e77955b6041db677238940e589b50",
10
14
  "battlegrid-strategy-doctor/SKILL.md": "14f20bff9e2b5a59d11634fe3615b1298d672d5e2eba06d3dc8698b571b3bee3",
11
- "battlegrid-strategy-examples/SKILL.md": "d49ed36a864d1ead53c942979639d331b2bf375b50fffd745548a0acde39d641",
12
- "battlegrid-strategy-examples/references/playbooks.md": "9e89b5cc8d80fc27bc331862fdc2403c26b36131b132614782c8425df74bcd28",
15
+ "battlegrid-strategy-examples/SKILL.md": "528049af46a0a829628681d16076129eb57e898445c803e4ebafae98699a3b65",
16
+ "battlegrid-strategy-examples/references/playbooks.md": "9e4038df885bb840d6ee58ba0475a9214dc58d12bf355c91c8aae625d58e3096",
13
17
  "battlegrid-strategy-examples/references/recipes.md": "61382acdd295fa159f055b787ba501f1c015f383f59a6714a973057f10de611f",
14
- "battlegrid-strategy-examples/references/tradingview-ports.md": "2b13adbc137178955edcca31b6ab40c01c745a932a0750b91db8820c736700bb",
15
- "battlegrid-trade-analysis/SKILL.md": "a1e666ca03bd641413096ed14806f101354f4e913e005faad6026a486143af0c",
18
+ "battlegrid-strategy-examples/references/tradingview-ports.md": "706a7dd3a120b2f3d5fa3190924f493cf4ebaf5df895bbeb155c2db73f8f5ece",
19
+ "battlegrid-trade-analysis/SKILL.md": "f545b916dc2a37c8fc35e16b27fa6f78ead4302e67f65ac584765c1e069b4ab2",
16
20
  "battlegrid-trade-proposal/SKILL.md": "d207a30065c17e13efaf5391805e5603e6956633cba5d0a1ff60e276518d9696"
17
21
  }
18
22
  }
@@ -125,7 +125,8 @@ mid-edit in their form**, and every change you make joins it. Then:
125
125
  read you proposed against returned (0 when it found no draft): it lands in their open form,
126
126
  labelled as yours. Each axis you send is written WHOLE — BEHAVIOR carries all three of risk,
127
127
  outlook and conviction, TRADING_CONFIG the complete configuration (step 6) — and the axes you
128
- omit keep their values.
128
+ omit keep their values. Complete stage payloads — a create, a behavior change, a rebind — are in
129
+ `references/agent-stage-examples.md`.
129
130
  - **Read the stage's diagnostics.** Its `errors` are the refusals the commit would raise, each
130
131
  naming its field: fix the axis and stage again before you ask for anything.
131
132
  - A staging call refused **`DRAFT_AXIS_CONTESTED`** names an axis the player changed after the
@@ -193,10 +194,13 @@ is voting on a state you have not seen. A commit refused with `nextAct: stage` n
193
194
  diagnostics named: fix it in a stage, read, and confirm again.
194
195
 
195
196
  **A refused archive names its own blockers.** The refusal carries typed `archiveBlockers[]` —
196
- DEPLOYED / OPEN_TRADES / ACTIVE_SESSION, each with a count. Report each blocker and its count as
197
- the reason, and name what would clear it (un-deploy via the radar/deployment tools, close or let
198
- the positions resolve, wait for the session to settle). Never paraphrase the refusal into "it
199
- didn't work", and never retry it unchanged.
197
+ DEPLOYED / OPEN_TRADES / ACTIVE_SESSION / RADAR_DEPLOYED, each with a count. Report each blocker and
198
+ its count as the reason, and name what would clear it (un-deploy via the deployment tools, close or
199
+ let the positions resolve, wait for the session to settle). RADAR_DEPLOYED counts paused radars too:
200
+ a paused radar still holds the slot, so pausing does not clear it. Clearing it is a radar-deployment
201
+ write, not this arc's: activate `radar-deployment` and let its arc find the coins whose radar holds
202
+ the agent and un-deploy it there — a radar draft committed without the agent, or the coin's
203
+ deployment deleted. Never paraphrase the refusal into "it didn't work", and never retry it unchanged.
200
204
 
201
205
  ### 5. Halt recovery: offer exactly the served verdict's exits
202
206
 
@@ -0,0 +1,64 @@
1
+ # Agent stage examples
2
+
3
+ Complete `stage_agent_draft` inputs. Each is checked in CI against the live stage schema; a create example is also
4
+ checked for every axis a new agent needs. Its shape is current.
5
+
6
+ Every `<UPPER_SNAKE>` value is a placeholder. Replace it with the id a read returned:
7
+
8
+ - `<AGENT_ID>` comes from the draft or the agent you are editing.
9
+ - `<MODEL_ID>` is the row's `modelId` from `list_approved_models`.
10
+ - `<STRATEGY_ID>` comes from the player's strategies.
11
+
12
+ Never send a placeholder as written, and never invent an id. `draftVersion` is the version `get_agent_draft`
13
+ returned: 0 when there was no draft, and for a new create draft. Each axis you send is written whole, and an axis
14
+ you omit keeps the value the draft already has.
15
+
16
+ ## Create a new agent
17
+
18
+ Omit `agentId` to open a create draft. The response carries the minted id, which every later call names. A new agent
19
+ needs all four of IDENTITY, BEHAVIOR, MODEL and STRATEGY_BINDING. TRADING_CONFIG is optional: omit it to take the
20
+ platform seed.
21
+
22
+ ```json stage_agent_draft
23
+ {
24
+ "request": {
25
+ "draftVersion": 0,
26
+ "axes": {
27
+ "IDENTITY": { "displayName": "Breakout Scout" },
28
+ "BEHAVIOR": { "behavior": { "risk": "MODERATE", "outlook": "REALIST", "conviction": "MEASURED" } },
29
+ "MODEL": { "modelId": "<MODEL_ID>" },
30
+ "STRATEGY_BINDING": { "strategyId": "<STRATEGY_ID>" }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Change an existing agent's behavior
37
+
38
+ BEHAVIOR is sent whole: all three of risk, outlook and conviction, even when only one changes.
39
+
40
+ ```json stage_agent_draft
41
+ {
42
+ "request": {
43
+ "agentId": "<AGENT_ID>",
44
+ "draftVersion": 4,
45
+ "axes": {
46
+ "BEHAVIOR": { "behavior": { "risk": "CONSERVATIVE", "outlook": "REALIST", "conviction": "CAUTIOUS" } }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## Rebind an existing agent to another strategy
53
+
54
+ ```json stage_agent_draft
55
+ {
56
+ "request": {
57
+ "agentId": "<AGENT_ID>",
58
+ "draftVersion": 4,
59
+ "axes": {
60
+ "STRATEGY_BINDING": { "strategyId": "<STRATEGY_ID>" }
61
+ }
62
+ }
63
+ }
64
+ ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: battlegrid-arena-play
3
- description: Enter the player into Market Grid sessions — find an open session, read its coin pool and live market context, compose a grid with real per-coin reasoning or have one of their agents generate it, submit it against the session's entry fee, then read the results and the reasoning journal after settlement. Activate whenever the player wants to play, enter, join or submit to a session, wants their agent to enter one, asks what is open to play, or asks how a session they entered turned out.
3
+ description: Enter the player into Market Grid sessions — find an open session, read its coin pool and live market context, compose a grid with real per-coin reasoning or have one of their agents generate it, submit it against the session's entry fee, then read the results and the player's own entry record after settlement. Activate whenever the player wants to play, enter, join or submit to a session, wants their agent to enter one, asks what is open to play, or asks how a session they entered turned out.
4
4
  ---
5
5
 
6
6
  # Arena Play
@@ -108,9 +108,12 @@ reasoning as the player's, and the attribution is part of what was scored.
108
108
  - `get_market_grid_results` — available only once the session is SETTLED. Before that it returns a
109
109
  typed CONFLICT naming the current status: **report it and stop**. Do not poll it in a loop; the
110
110
  op budget is finite and a settling session is not a failure.
111
- - `get_mcp_reasoning_journal` on request — the player's own submission: the reasoning, confidence
112
- and picks as recorded at submit. Compare the outcome against what was actually reasoned, and say
113
- where the reasoning was wrong. An agent's record is a different journal, `get_agent_journal`.
111
+ - `get_my_market_grid_entry` on request — the player's own entry in that session: its picks, who
112
+ authored it, whether another author has rewritten it (`edited`), and the reasoning and confidence
113
+ recorded at submit. Compare the outcome against what was actually reasoned, and say where the
114
+ reasoning was wrong. `reasoning` is null once the entry was edited — the record no longer describes
115
+ the picks, so say that rather than reconstructing one. `list_my_market_grid_entries` is the history
116
+ across sessions. An agent's decisions are a different record, `get_agent_journal`.
114
117
 
115
118
  ## Refusals, and what each one means
116
119
 
@@ -113,6 +113,20 @@ is test-routing a regime. Never carry a fact from a radar conversation into it,
113
113
  To try a slot set without touching the draft, preview `{ kind: "SLOTS", slots }` — it resolves
114
114
  explicit slots and nothing can commit it.
115
115
 
116
+ **Shaping a Radar rule.** A rule carries one or two conditions — never none. Pick the construction
117
+ from what the player asked for:
118
+
119
+ | The player wants the agent on duty… | Construction |
120
+ |---|---|
121
+ | whenever no rule matches | `DEFAULT_SLOT` — on duty only while no rule matches, so on a coin with no rules it is on duty at every instant |
122
+ | only in named regimes | a rule with a regime condition |
123
+ | only in named hours and days | a rule with an hours condition |
124
+ | in named regimes during named hours | a rule with one hours condition and one regime condition |
125
+
126
+ When the ask maps to no single row, or a refusal rules out the construction the player asked for,
127
+ ask the player which construction they mean, naming the rows that fit — never stage one they did not
128
+ choose. Complete stage payloads for every row are in `references/radar-stage-examples.md`.
129
+
116
130
  **Arena** — the same shape, keyed on `presetId`:
117
131
 
118
132
  1. `stage_deployment_policy_draft` with `draftVersion` = the `version` you read (0 when there was no
@@ -127,6 +141,21 @@ explicit slots and nothing can commit it.
127
141
  A draft that would hold no rule and no catch-all is a withdrawal, which a commit never does — that is
128
142
  `delete_deployment_policy` (step 4).
129
143
 
144
+ **Shaping an Arena rule.** A rule needs at least one condition by the time it is committed — a draft
145
+ may hold a rule with none while the player is still choosing, and the draft read's diagnostics flag
146
+ it. Pick the construction from what the player asked for:
147
+
148
+ | The player wants the agent to play… | Construction |
149
+ |---|---|
150
+ | whenever no rule matches | `DEFAULT_SLOT` — plays only when no rule wins the session |
151
+ | only in named regimes | a rule with a regime condition |
152
+ | only at named session starts, or one scheduled occurrence | a rule with one session condition — a start or an occurrence |
153
+ | in named regimes at those sessions | a rule with a regime condition and one session condition |
154
+
155
+ When the ask maps to no single row, or a refusal rules out the construction the player asked for,
156
+ ask the player which construction they mean, naming the rows that fit — never stage one they did not
157
+ choose. Complete stage payloads for every row are in `references/arena-stage-examples.md`.
158
+
130
159
  Both previews run the **same resolver the live sweep runs**, so the preview is the real outcome, not
131
160
  an estimate. Neither writes anything and neither costs the player an LLM call — so previewing
132
161
  repeatedly while iterating is free and correct.
@@ -0,0 +1,140 @@
1
+ # Arena stage examples
2
+
3
+ Complete `stage_deployment_policy_draft` inputs, one per construction in **Shaping an Arena rule**. Each is checked in
4
+ CI against the live stage schema and composed into a deployment the aggregate accepts, so its shape is current.
5
+
6
+ Every `<UPPER_SNAKE>` value is a placeholder. Replace it with the id a read returned:
7
+
8
+ - `<PRESET_ID>` comes from `list_game_presets`.
9
+ - `<AGENT_ID>` comes from your agent list.
10
+ - `<COIN_ID>` comes from the arena's coin pool.
11
+
12
+ Never send a placeholder as written, and never invent an id. `draftVersion` is the version
13
+ `get_deployment_policy_draft` returned: 0 when the arena has no draft. Each axis you send is written whole, and an
14
+ axis you omit keeps the value the draft already has.
15
+
16
+ ## Play whenever no rule matches — the default slot
17
+
18
+ ```json stage_deployment_policy_draft
19
+ {
20
+ "request": {
21
+ "presetId": "<PRESET_ID>",
22
+ "draftVersion": 0,
23
+ "axes": {
24
+ "DEFAULT_SLOT": {
25
+ "defaultSlot": { "agentId": "<AGENT_ID>", "minConfidence": null, "entryStrategy": "STANDARD" }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## Play only in named regimes
33
+
34
+ ```json stage_deployment_policy_draft
35
+ {
36
+ "request": {
37
+ "presetId": "<PRESET_ID>",
38
+ "draftVersion": 0,
39
+ "axes": {
40
+ "RULES": {
41
+ "rules": [
42
+ {
43
+ "agentId": "<AGENT_ID>",
44
+ "minConfidence": null,
45
+ "entryStrategy": "STANDARD",
46
+ "conditions": [{ "kind": "regime", "regimes": ["bull_expansion", "bull_ranging"] }]
47
+ }
48
+ ]
49
+ }
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Play only at named session starts
56
+
57
+ Start times are UTC `HH:MM:SS`. Days run 0 (Sunday) to 6. Each is listed once. A rule holds a session start or a
58
+ session occurrence, never both.
59
+
60
+ ```json stage_deployment_policy_draft
61
+ {
62
+ "request": {
63
+ "presetId": "<PRESET_ID>",
64
+ "draftVersion": 0,
65
+ "axes": {
66
+ "RULES": {
67
+ "rules": [
68
+ {
69
+ "agentId": "<AGENT_ID>",
70
+ "minConfidence": 0.6,
71
+ "entryStrategy": "TWO_LOOK",
72
+ "conditions": [
73
+ { "kind": "session_start", "startTimes": ["14:00:00", "15:00:00"], "days": [1, 2, 3, 4, 5] }
74
+ ]
75
+ }
76
+ ]
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## In named regimes at those sessions — a regime condition and one session condition
84
+
85
+ ```json stage_deployment_policy_draft
86
+ {
87
+ "request": {
88
+ "presetId": "<PRESET_ID>",
89
+ "draftVersion": 0,
90
+ "axes": {
91
+ "RULES": {
92
+ "rules": [
93
+ {
94
+ "agentId": "<AGENT_ID>",
95
+ "minConfidence": null,
96
+ "entryStrategy": "STANDARD",
97
+ "conditions": [
98
+ { "kind": "regime", "regimes": ["volatile"] },
99
+ { "kind": "session_start", "startTimes": ["14:00:00"], "days": [1, 2, 3, 4, 5] }
100
+ ]
101
+ }
102
+ ]
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## Override the regime anchor on a deployed arena
110
+
111
+ This stages only `REGIME_ANCHOR`, so the deployed rules and default slot stay as they are. The coin and the timeframe
112
+ come together, or not at all.
113
+
114
+ ```json stage_deployment_policy_draft
115
+ {
116
+ "request": {
117
+ "presetId": "<PRESET_ID>",
118
+ "draftVersion": 2,
119
+ "axes": {
120
+ "REGIME_ANCHOR": { "regimeAnchor": { "regimeReferenceCoinId": "<COIN_ID>", "regimeTimeframe": "4h" } }
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## Clear the regime anchor override
127
+
128
+ The arena's own anchor applies again.
129
+
130
+ ```json stage_deployment_policy_draft
131
+ {
132
+ "request": {
133
+ "presetId": "<PRESET_ID>",
134
+ "draftVersion": 2,
135
+ "axes": {
136
+ "REGIME_ANCHOR": { "regimeAnchor": null }
137
+ }
138
+ }
139
+ }
140
+ ```
@@ -0,0 +1,155 @@
1
+ # Radar stage examples
2
+
3
+ Complete `stage_radar_deployment_draft` inputs, one per construction in **Shaping a Radar rule**. Each is
4
+ checked in CI against the live stage schema and composed into a policy the aggregate accepts, so its
5
+ shape is current.
6
+
7
+ Every `<UPPER_SNAKE>` value is a placeholder. Replace it with the id a read returned:
8
+
9
+ - `<COIN_ID>` comes from `get_coin_metadata`.
10
+ - `<AGENT_ID>` comes from your agent list.
11
+ - `<OTHER_AGENT_ID>` is a second agent from your agent list, where an example deploys two.
12
+
13
+ Never send a placeholder as written, and never invent an id. `draftVersion` is the version
14
+ `get_radar_deployment_draft` returned: 0 when the coin has no draft. Each axis you send is written whole, and an axis
15
+ you omit keeps the value the draft already has.
16
+
17
+ ## On duty whenever no rule matches — the default slot
18
+
19
+ On a coin with no rules, the default slot is on duty at every instant. This is the construction for "put this agent
20
+ on this coin" with no condition named.
21
+
22
+ ```json stage_radar_deployment_draft
23
+ {
24
+ "request": {
25
+ "coinId": "<COIN_ID>",
26
+ "draftVersion": 0,
27
+ "axes": {
28
+ "DEFAULT_SLOT": { "defaultSlot": { "agentId": "<AGENT_ID>", "minConviction": null } }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## On duty only in named regimes
35
+
36
+ `minConviction` on the rule is the trade bar (0–1, or null to inherit the agent's). `minConviction` inside the
37
+ regime condition is the regime-reading floor, and must be a level one of the selected regimes can emit.
38
+
39
+ ```json stage_radar_deployment_draft
40
+ {
41
+ "request": {
42
+ "coinId": "<COIN_ID>",
43
+ "draftVersion": 0,
44
+ "axes": {
45
+ "RULES": {
46
+ "rules": [
47
+ {
48
+ "agentId": "<AGENT_ID>",
49
+ "minConviction": null,
50
+ "conditions": [
51
+ { "kind": "regime", "regimes": ["bull_expansion", "bull_ranging"], "minConviction": "high" }
52
+ ]
53
+ }
54
+ ]
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## On duty only in named hours and days
62
+
63
+ Hours are UTC, 0–23. Days run 0 (Sunday) to 6. Each is listed once.
64
+
65
+ ```json stage_radar_deployment_draft
66
+ {
67
+ "request": {
68
+ "coinId": "<COIN_ID>",
69
+ "draftVersion": 0,
70
+ "axes": {
71
+ "RULES": {
72
+ "rules": [
73
+ {
74
+ "agentId": "<AGENT_ID>",
75
+ "minConviction": null,
76
+ "conditions": [
77
+ { "kind": "hours", "hours": [13, 14, 15, 16, 17, 18, 19, 20], "days": [1, 2, 3, 4, 5] }
78
+ ]
79
+ }
80
+ ]
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ## In named regimes during named hours — one condition of each kind
88
+
89
+ ```json stage_radar_deployment_draft
90
+ {
91
+ "request": {
92
+ "coinId": "<COIN_ID>",
93
+ "draftVersion": 0,
94
+ "axes": {
95
+ "RULES": {
96
+ "rules": [
97
+ {
98
+ "agentId": "<AGENT_ID>",
99
+ "minConviction": 0.6,
100
+ "conditions": [
101
+ { "kind": "hours", "hours": [13, 14, 15, 16], "days": [1, 2, 3, 4, 5] },
102
+ { "kind": "regime", "regimes": ["volatile"] }
103
+ ]
104
+ }
105
+ ]
106
+ }
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Rules plus a default slot
113
+
114
+ Every rule that matches is on duty, and the first rule listed is offered the coin's fire first. The default slot is
115
+ on duty only while no rule matches, so here the second agent is on duty whenever the coin is in neither of the
116
+ first agent's regimes.
117
+
118
+ ```json stage_radar_deployment_draft
119
+ {
120
+ "request": {
121
+ "coinId": "<COIN_ID>",
122
+ "draftVersion": 0,
123
+ "axes": {
124
+ "RULES": {
125
+ "rules": [
126
+ {
127
+ "agentId": "<AGENT_ID>",
128
+ "minConviction": null,
129
+ "conditions": [{ "kind": "regime", "regimes": ["bear_expansion", "bear_ranging"] }]
130
+ }
131
+ ]
132
+ },
133
+ "DEFAULT_SLOT": { "defaultSlot": { "agentId": "<OTHER_AGENT_ID>", "minConviction": null } }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## Clear the default slot on a deployed coin
140
+
141
+ This stages only `DEFAULT_SLOT`, so the deployed rules stay as they are. Send `RULES` only when the rules change, and
142
+ then send the complete list: a rule you do not resend is deleted. `draftVersion` here is the version your read
143
+ returned.
144
+
145
+ ```json stage_radar_deployment_draft
146
+ {
147
+ "request": {
148
+ "coinId": "<COIN_ID>",
149
+ "draftVersion": 3,
150
+ "axes": {
151
+ "DEFAULT_SLOT": { "defaultSlot": null }
152
+ }
153
+ }
154
+ }
155
+ ```
@@ -129,12 +129,29 @@ question you would otherwise guess — and a refusal you would otherwise earn:
129
129
  it — its own `hold: { atLeast, of }`, which the editor calls **Hold** (Once, In a row, Within, At
130
130
  least), counted in completed bars of the column's own timeframe — not to this axis. Every clause
131
131
  and group carries `hold`; `{ "atLeast": 1, "of": 1 }` is Once. The `strategy-examples` skill
132
- carries the vocabulary, the served hold domain (a crossover is held "within" only; a Developing
133
- column cannot be held) and the per-column Confirmed / Developing read.
132
+ carries the vocabulary, the served hold domain (a crossover or an edge operator — `crossesAbove`,
133
+ `crossesBelow`, `enters`, `exits` — is held "within" only; a Developing column cannot be held) and
134
+ the per-column Confirmed / Developing read.
135
+ - **What a bar did is its own vocabulary.** A touch or a rejection of a level, a candle pattern, a
136
+ gap and a run of bars are composed from the `strategy-examples` skill's **Bar geometry** section —
137
+ `levelInteraction` over a level with its `buffer` knob, and the pattern, gap, run and price-source
138
+ metrics. A level BREAK is a `distance` crossing over the same level, never a `levelInteraction`
139
+ label.
140
+ - **Swing structure is its own section.** Swing highs and lows, the `HH`/`HL`/`LH`/`LL` label and the
141
+ break-of-structure recipes are in the `strategy-examples` skill's **Swing structure** section.
134
142
  - `get_strategy_column_contract` → `outputs[].conditionOperators`. An empty array means that
135
143
  rendered header has no comparison semantics and cannot appear in a condition clause at all.
136
144
  Legality is per rendered header, not per column: a trajectory's slot header and its `_trend`
137
145
  header answer differently.
146
+ - `get_strategy_column_contract` → `lookback`, `measurement` and `seriesHome`. `lookback.used` is
147
+ the bars the column reads against `lookback.cap`, the column-lookback budget a save holds it to —
148
+ a column past the cap is refused with `REPORT_COLUMN_LOOKBACK_EXCEEDED` by this tool exactly as by
149
+ the save, so shrink its window or offset rather than staging it. `measurement` names how the
150
+ transform (and a chained transform) reads its series: the read shape, the window's unit, the price
151
+ basis and what an absent bar does. `seriesHome` says where the metric's series is kept — `candle`,
152
+ `history`, or `null` when the metric has no series, which is why a series transform does not
153
+ compose on it. Window and offset carry a declared `minimum` and no fixed maximum; the lookback
154
+ cap is the bound.
138
155
  - `get_metric_construction_hints` → `rankOrderings`. Present only when rank is composable on that
139
156
  metric, already range-gated server-side — read the offered set rather than deriving one from
140
157
  the metric's native output.
@@ -160,7 +177,8 @@ asked to approve.
160
177
  the `axes` you propose. It writes those axes into the player's draft — attributed to you, landing in
161
178
  their builder as unsaved changes — and commits nothing. Each stage returns the draft's new
162
179
  `version`: that is the `draftVersion` your next stage names. Stage each change once, and never fire
163
- two stages in parallel against one draft.
180
+ two stages in parallel against one draft. Complete stage payloads showing the envelope are in
181
+ `references/strategy-stage-examples.md`.
164
182
 
165
183
  **A new strategy is a create draft.** Omit `strategyId` and send `draftVersion: 0` on the first
166
184
  stage; the response carries the id minted for it, which every later call names. A create needs
@@ -0,0 +1,50 @@
1
+ # Strategy stage examples
2
+
3
+ `strategyId` and `draftVersion` sit beside `axes`, inside `request` — never inside `axes`. Every example below is
4
+ checked in CI against the live `stage_strategy_draft` schema; a create example is also checked for the axes a new
5
+ strategy needs. Its shape is current.
6
+
7
+ These examples show the envelope and the small axes. A full composition (report sections, conditions, signal
8
+ rules) is in `strategy-examples`' playbooks, which have their own compile gate.
9
+
10
+ Every `<UPPER_SNAKE>` value is a placeholder: `<STRATEGY_ID>` is the id a read returned. Never send a placeholder as
11
+ written, and never invent an id. `draftVersion` is the version `get_strategy_draft` returned: 0 when there was no
12
+ draft, and for a new create draft. Each axis you send is written whole, and an axis you omit keeps the value the draft
13
+ already has.
14
+
15
+ ## Open a create draft
16
+
17
+ Omit `strategyId`. The response carries the minted id, which every later call names. A new strategy needs IDENTITY
18
+ and TIMEFRAME_PROFILE before it can be created.
19
+
20
+ ```json stage_strategy_draft
21
+ {
22
+ "request": {
23
+ "draftVersion": 0,
24
+ "axes": {
25
+ "IDENTITY": {
26
+ "name": "Squeeze Breakout",
27
+ "description": "Enters when a volatility squeeze resolves upward on rising volume.",
28
+ "tagline": "Buys the release of a tight range"
29
+ },
30
+ "TIMEFRAME_PROFILE": { "timeframe": "1h" }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Change an existing strategy's setup gates
37
+
38
+ SETUP_GATES is sent whole: all three gates, even when only one changes.
39
+
40
+ ```json stage_strategy_draft
41
+ {
42
+ "request": {
43
+ "strategyId": "<STRATEGY_ID>",
44
+ "draftVersion": 2,
45
+ "axes": {
46
+ "SETUP_GATES": { "minAggregateScore": 60, "minRequiredCount": 1, "minAtrPct": 0.5 }
47
+ }
48
+ }
49
+ }
50
+ ```
@@ -44,10 +44,11 @@ Before committing a CREATE, every "no" here is a decision to state, not an omiss
44
44
  | `efficiency` wN | `<code>_er` (1 straight, ~0 chop) | `close_ltf_er` |
45
45
  | `maxShare` wN | `<code>_maxShare` | `volBase_ltf_maxShare` |
46
46
  | `classifyZone` / `classifyState` | `<code>_zone` / `<code>_state` | `RSI14_zone`, `ADX_state` |
47
+ | `levelInteraction` | `<code>_lvl`, `<code>_lvl_b<N>` at a buffer of N bps | `bbUpper_lvl`, `bbUpper_lvl_b25` |
47
48
 
48
49
  Non-anchor rungs affix `_ltf` (lower) / `_htf` (regime): `MAalign_htf`,
49
50
  `zones_htf_support_dist`. Chains are bounded at two: inner `distance`/`spread` → outer
50
- `trajectory`/`aggregate`/`efficiency`/`maxShare`/`rank` (`EMA5 spread EMA13 × trajectory` →
51
+ `trajectory`/`aggregate`/`rank` (`EMA5 spread EMA13 × trajectory` →
51
52
  `EMA5_EMA13_spread_now` + `_trend`).
52
53
 
53
54
  **Timeframe references are two families.** *Relative* (`anchor`/`lower`/`regime`) re-resolve
@@ -84,7 +85,11 @@ no timeframe reference at all: bind them `{rel: 'anchor'}` — the default — a
84
85
  previous UTC session from any anchor. An absolute reference is REFUSED at save, including `1d`,
85
86
  because there is no rung for one to select. They are catalogued price levels, so `dist_PDH gte 0`
86
87
  composes directly. (`distance` still rejects an `offset`, and a clause still compares a column
87
- against a literal; neither of those shapes is what a previous-session level needed.)
88
+ with a literal or with its own previous completed bar, never with another column; neither of those
89
+ shapes is what a previous-session level needed.) The same holds
90
+ for every timeless metric — these levels, funding, the regime and the other bundle
91
+ reads: a `value` column over one accepts only `offset: 0`, and a larger offset is REFUSED at save,
92
+ because there is no earlier bar to cut back to.
88
93
 
89
94
  ## Conditions
90
95
 
@@ -92,8 +97,11 @@ against a literal; neither of those shapes is what a previous-session level need
92
97
  required, no defaults. A `clock` key is REFUSED (the per-condition evidence clock was retired in
93
98
  contract `61.0.0`), so is a condition-level `closes` key — the hold lives on each clause and each
94
99
  group (contract `84.0.0`) — and so is a boolean `exit` key: the side an exit closes is its own
95
- `exitSide` (contract `88.0.0`). Clauses: numeric/rank headers take `lt|lte|gte|gt|between`;
96
- classification/direction headers take `is|in` with the served vocabulary. Groups:
100
+ `exitSide` (contract `88.0.0`). Clauses — the operator a header takes is served per header
101
+ (`conditionOperators`), by its output kind: numeric headers take the comparisons
102
+ `lt|lte|gte|gt|between` and the prior-bar operators `crossesAbove|crossesBelow|increased|decreased`;
103
+ rank headers take the comparisons only; classification, direction and boolean headers take
104
+ `is|in|enters|exits` with the served vocabulary; event headers take `is|in`. Groups:
97
105
  `ALL | ANY | NOT | N_OF` (with `n`), depth ≤ 2. Every clause and every group carries a required
98
106
  `hold: { atLeast, of }` — `{ "atLeast": 1, "of": 1 }` is a single read; a clause or group without
99
107
  `hold` is refused (`CONDITION_HOLD_INVALID`). `conditionRef` composes named conditions (no
@@ -104,6 +112,46 @@ decider — so order breaks ties between carriers that AGREE. Carriers that DISA
104
112
  `NEITHER` and stand the coin aside; declaration order never picks a side between them. Building
105
113
  blocks carry `null`.
106
114
 
115
+ **Prior-bar operators — a column against its own previous completed bar.** A clause compares its
116
+ column with a literal, or with the same column's reading on the completed bar before the one it
117
+ decides — never with another column. Six operators read that prior bar:
118
+
119
+ | Operator | Literal | TRUE when |
120
+ |---|---|---|
121
+ | `crossesAbove` | a number `L` | `x > L` on the decided bar and `x ≤ L` on the bar before |
122
+ | `crossesBelow` | a number `L` | `x < L` on the decided bar and `x ≥ L` on the bar before |
123
+ | `increased` | none | `x` is strictly above its prior-bar reading |
124
+ | `decreased` | none | `x` is strictly below its prior-bar reading |
125
+ | `enters` | one label | the label is read now and was not on the bar before |
126
+ | `exits` | one label | the label was read on the bar before and is not now |
127
+
128
+ The crossing boundary is Pine's (`ta.crossover`): a bar sitting exactly on `L` has not crossed until a
129
+ later bar closes above it. A change takes no literal at all — `{ "op": "increased" }` with no `value`.
130
+ A prior-bar clause reads COMPLETED bars only, on every surface: the decided bar and the one before
131
+ it, never the forming bar, so it is never provisional. A failed read on either bar is UNRESOLVED; a
132
+ bar with no value (a null) reads FALSE. A header admits them only where its stored history keeps the
133
+ bar before: a header whose `conditionHold.maxWindow` is 1 is served none of the six, so
134
+ `vol24hUsd increased` is refused (`CONDITION_OPERATOR_UNSUPPORTED`, naming the operators it does
135
+ admit), and on a header that keeps them each reads one bar less than its window.
136
+
137
+ **An edge is held "within n closes" only.** `crossesAbove`, `crossesBelow`, `enters` and `exits` mark
138
+ the bar a state changed, so like a crossover event their `maxAtLeast` is 1 — hold them "within n
139
+ closes", never "in a row". `increased` and `decreased` are no edge: "3 closes in a row" of
140
+ `increased` is three rising closes. A group whose members must all read TRUE together (`ALL`) over an
141
+ edge admits "within" only too. Every prior-bar read counts toward the frame-read budget: a clause
142
+ held "within 3 closes" reads four bars.
143
+
144
+ Recipes — each one clause:
145
+
146
+ - **Close crossed above the 20 SMA** — `dist_SMA20 crossesAbove 0` (the Moving Averages module).
147
+ `dist_SMA20` is the close's distance from the average, so crossing 0 is the close crossing it.
148
+ - **RSI entered overbought** — `RSI14_zone enters "overbought"` (the RSI module).
149
+ - **Squeeze released** — `kcSqueeze exits "on"`, over a `KC_SQUEEZE` value column.
150
+ - **New 20-bar high** — `donchianHi increased`, over a `DONCHIAN_UPPER` value column: the 20-bar
151
+ upper channel rises exactly when the decided bar's high exceeds every high of the 20 bars before it.
152
+ - **RSI crossed above 50 within the last 3 closes** — `RSI14_now crossesAbove 50` holding
153
+ `{ "atLeast": 1, "of": 3 }`.
154
+
107
155
  **Two things block a trade, not one.** `required: true` = a FALSE reading blocks compose-trade
108
156
  before billing. And the RESOLVED verdict binds entry DIRECTION: `UP` admits long setups only,
109
157
  `DOWN` short only, and `NEITHER`/`UNRESOLVED` admit none — the refused side is absent from the
@@ -247,10 +295,12 @@ it opened under, with the report and timeframes they read: an exit-rule edit app
247
295
  opened after it, and an open position keeps the exit rules it opened with, so an open position never
248
296
  refuses a commit or a rebind.
249
297
 
250
- **A state column is not a flip event.** `ST_DIR` reads the same on every bar of a trend, so
251
- `ST_DIR is "bullish"` is a regime filter and never an entry signal. The flip needs an event column
252
- beside it — an `EMA5_13` cross, or a `ST_DIR` trajectory whose `_trend` changes. The same distinction
253
- applies to every persisting classification: `MAalign`, `ADX_state`, `zone`.
298
+ **A state column is not a flip event — `is` reads the state, `enters` reads the flip.** `ST_DIR`
299
+ reads the same on every bar of a trend, so `ST_DIR is "bullish"` is a regime filter and never an
300
+ entry signal. The flip is `ST_DIR enters "bullish"`: TRUE on the one completed bar the direction
301
+ turned, with no event column needed. The same distinction applies to every persisting
302
+ classification: `MAalign`, `ADX_state`, `zone` — `is` for the regime, `enters` / `exits` for the
303
+ bar it changed.
254
304
 
255
305
  **Name the anchor when a metric is calibrated for one.** `KC_SQUEEZE is "on"` reads *on* about 57% of
256
306
  1h crypto bars even at canonical parameters — Bollinger σ is close-to-close while ATR captures
@@ -258,6 +308,145 @@ intrabar range, so σ/ATR runs low here — against about 19% at 15m. It is a se
258
308
  below and close to useless at 1h. A metric whose selectivity depends on the anchor is stated with the
259
309
  anchor, or the author gates on something that admits most bars.
260
310
 
311
+ ## Bar geometry
312
+
313
+ What one bar did — against a price level, in its own shape, against the bar before it, and how many
314
+ bars ran one way. Every value here reads the bar's own open, high, low and close, so it is a candle
315
+ column: it takes a timeframe reference and the Confirmed / Developing selector like any other.
316
+
317
+ **`levelInteraction` — what the bar did at a level.** It classifies the newest bar of the column as
318
+ one of five labels: `touch_from_below`, `touch_from_above`, `reject_from_below`, `reject_from_above`,
319
+ `none`. The side is where the PRIOR close stood against the PRIOR bar's level — from below when it
320
+ closed at or under it — so a level that moved between the two bars is judged on the bar it stood at.
321
+ From below: a bar that closes through the level is a break and reads `none`; a bar whose high
322
+ reached the level and whose close came back under it is `reject_from_below`; a bar whose high came
323
+ within the buffer of the level is `touch_from_below`. From above is the mirror on the low.
324
+
325
+ - **A reject bar reports reject.** It is never also reported as a touch, so "touched or rejected" is
326
+ an `in` clause over both labels of a side: `bbUpper_lvl in ["touch_from_below","reject_from_below"]`.
327
+ - **`buffer`** is whole basis points of the level, 0–500, default 0 — `buffer: 25` is 0.25%. Only the
328
+ touch reads it: a reject must pierce the level itself. Two buffers are two columns (`bbUpper_lvl`,
329
+ `bbUpper_lvl_b25`).
330
+ - **A break is a crossing, never a label.** The break of a level is `distance` over the same level
331
+ crossing 0: `dist_bbUpper crossesAbove 0` from below, `crossesBelow 0` from above. On the strategy's
332
+ own rung a `…_from_below` label never holds on a bar where `crossesAbove 0` holds.
333
+ - **Which levels take it.** A candle-homed level — a band (`BB_UPPER`, `BB_LOWER`, `KC_UPPER`,
334
+ `KC_MID`, `KC_LOWER`), `VWAP`, a moving average (`SMA20`, `EMA20`, …), `ST_LINE`, `PSAR`, the
335
+ Ichimoku lines, the swing levels (`SWING_FRACTAL_HIGH`, `SWING_FRACTAL_LOW`) — reads its own value at
336
+ each of the two bars. A prior-session level or pivot (`PDH`,
337
+ `PDL`, `PDO`, `PIVOT_P`, `PRIOR_TPO_POC`, …) has one reading for the session, used as both bars' level;
338
+ a clause over it is a single read with no hold, because the session profile keeps no earlier
339
+ reading. A developing-session level (`TPO_POC`, `TPO_VAH`, …) and the naked points of control move
340
+ between bars with no earlier reading kept, so they are not offered it; neither are the Donchian
341
+ rails (`DONCHIAN_UPPER`, `DONCHIAN_LOWER`), each the extreme of the window ending at the very bar it
342
+ would classify, which a close can never pass. A price fact (`CLOSE`, `HLC3`, `MARK`) is refused: it
343
+ is the price, not a level.
344
+ - **A higher-rung level reads that rung's completed bar.** `bbUpper_htf_lvl` classifies the last
345
+ completed regime-rung bar from its own high, low and close, never the strategy bar's price — so
346
+ compose its break on the same rung, `dist_bbUpper_htf crossesAbove 0`.
347
+
348
+ **`BB_TOUCH` and `BB_UPPER × levelInteraction` answer different questions.** `BB_TOUCH` (`BBtouch`)
349
+ says where the CLOSE sits in %B — inside the band edge or not. `BB_UPPER × levelInteraction` says what
350
+ the bar's RANGE did at the edge — touched it, or pierced it and closed back.
351
+
352
+ **Candle patterns** — the bar's shape against the ten bars before it (TA-Lib's default settings). A
353
+ pattern carries no trend context: "a hammer in a downtrend" is `pin is "lower_wick"` plus a separate
354
+ trend clause.
355
+
356
+ | Key | Header | Labels |
357
+ |---|---|---|
358
+ | `CANDLE_DOJI` | `doji` | boolean: the body is at most a tenth of the average range |
359
+ | `CANDLE_PIN` | `pin` | `lower_wick`, `upper_wick`, `none` — a short body with one long wick |
360
+ | `CANDLE_ENGULFING` | `engulf` | `bullish`, `bearish`, `none` — the body engulfs the prior opposite body |
361
+ | `BAR_CONTAINMENT` | `barContain` | `inside`, `outside`, `none` — the range inside, or around, the prior range |
362
+
363
+ **Gaps** — the open against the bar before it, as three separate keys.
364
+
365
+ | Key | Header | Reads |
366
+ |---|---|---|
367
+ | `GAP` | `gap` | `up`, `down`, `none` — the open above the prior high, or below the prior low |
368
+ | `GAP_RANGE_CLEAR` | `gapClear` | `up`, `down`, `none` — the whole range clear of the prior range (stricter) |
369
+ | `GAP_PCT` | `gapPct` | the signed % from the prior close to the open |
370
+
371
+ **Runs** — report columns counting consecutive bars, capped at ±20. Gating "three closes in a row" is
372
+ the hold's job; a run column shows the streak.
373
+
374
+ | Key | Header | Counts |
375
+ |---|---|---|
376
+ | `CLOSE_RUN` | `closeRun` | consecutive closes above (+) or below (−) the close before |
377
+ | `CANDLE_RUN` | `candleRun` | consecutive candles of the newest bar's colour, white (+) or black (−) |
378
+
379
+ **Price sources** — per-bar price facts that take every transform a close takes: `HL2` (`hl2`,
380
+ median), `HLC3` (`hlc3`, typical), `OHLC4` (`ohlc4`, average).
381
+
382
+ **"Within the last N closed bars" is the hold `{ "atLeast": 1, "of": N }`.** A pattern or a gap held
383
+ within N reads each of the N bars against its own predecessor.
384
+
385
+ Recipes — the level recipes on candle-homed levels:
386
+
387
+ - **Price rejected the upper Bollinger band** — a `BB_UPPER × levelInteraction` column, header
388
+ `bbUpper_lvl`; the clause `bbUpper_lvl is "reject_from_below"`.
389
+ - **Price touched or rejected VWAP from above** — `VWAP_lvl in ["touch_from_above","reject_from_above"]`,
390
+ with `buffer: 10` for a touch within 0.1% (header `VWAP_lvl_b10`).
391
+ - **Price broke the 20 EMA** — `dist_EMA20 crossesAbove 0`, the `distance` column over the same level.
392
+ - **A bullish engulfing within the last 3 bars** — `engulf is "bullish"` holding
393
+ `{ "atLeast": 1, "of": 3 }`.
394
+ - **An inside bar** — `barContain is "inside"`.
395
+ - **A gap up** — `gap is "up"`; `gapClear is "up"` when the whole range must clear.
396
+ - **A run of candles as a report column** — a `CANDLE_RUN` value column (`candleRun`) beside the
397
+ thesis columns; no clause needed.
398
+ - **The 20-bar simple moving average of typical price** — `HLC3 × aggregate(20)` with
399
+ `aggregate: "mean"`, header `hlc3_mean20`.
400
+
401
+ ## Swing structure
402
+
403
+ Swing highs, swing lows and the structure they spell, from the market-data swing engine. Every value
404
+ is stored under the bar it describes, so a swing column holds and takes the prior-bar operators like
405
+ any candle column.
406
+
407
+ | Key | Header | Reads |
408
+ |---|---|---|
409
+ | `SWING_FRACTAL_HIGH` | `fractalHi` | the most recent swing high — a price level |
410
+ | `SWING_FRACTAL_LOW` | `fractalLo` | the most recent swing low — a price level |
411
+ | `SWING_FRACTAL_LABEL` | `fractalLabel` | `HH`, `HL`, `LH`, `LL`, `none` — the most recent swing against the one before it on its side |
412
+
413
+ **The rule.** A swing high is a bar whose high is at or above the highs of the two bars before it and
414
+ strictly above the highs of the two bars after it; a swing low mirrors it on the lows. An equal extreme
415
+ is allowed on the left and not on the right, so of a run of equal highs the newest is the swing (Pine's
416
+ `ta.pivothigh(2, 2)`). A swing is published from the bar that completes its right side — two bars
417
+ after its own — never earlier and never from a forming bar. Each level is held until the next swing on
418
+ its side replaces it.
419
+
420
+ - **The label is a held state, not an event.** It reads the most recent swing: `HH` / `LH` for a high
421
+ (an equal high is `LH` — it failed to make a higher high), `HL` / `LL` for a low (an equal low is
422
+ `HL`), `none` when no comparable pair is in reach. On too short a history it reads nothing, and a
423
+ clause over it reads FALSE.
424
+ - **A swing level does not remember a broken level.** Two to four bars after a break the level steps
425
+ to the next swing, so a retest of the BROKEN level is not a swing-level read.
426
+ - **The two levels take what every candle level takes** — `distance`, `spread`, `aggregate`,
427
+ `trajectory`, `efficiency` and `levelInteraction` — and both rank across the board
428
+ (`dist_fractalHi_rank_near`: the coins closest to their own last swing high).
429
+ - **The break is the crossing toward the level.** `dist_fractalHi crossesAbove 0` is a close through the
430
+ last swing high and never fires when the level steps onto a new swing, because the bar that confirms
431
+ a swing high sits under it. The opposite crossing (`dist_fractalHi crossesBelow 0`, or
432
+ `dist_fractalLo crossesAbove 0`) CAN fire on the bar the level steps past the close — it is not a
433
+ break.
434
+ - **A swing confirmed on the break bar can already have moved the label**, so a structure condition
435
+ paired with a break is held over the bars before it.
436
+
437
+ Recipes:
438
+
439
+ - **The swing structure is bullish** — `fractalLabel in ["HH","HL"]`.
440
+ - **A higher high was confirmed on this bar** — `fractalHi increased` (it also catches a second
441
+ higher high in a row, which `fractalLabel enters "HH"` cannot); `fractalHi decreased` is a lower
442
+ high.
443
+ - **Bullish break of structure** — a `SWING_FRACTAL_HIGH × distance` column, header `dist_fractalHi`;
444
+ the clause `dist_fractalHi crossesAbove 0`.
445
+ - **Bearish break of structure** — `dist_fractalLo crossesBelow 0`.
446
+ - **A break of the last swing low while the structure was bullish** — `dist_fractalLo crossesBelow 0`
447
+ together with `fractalLabel in ["HH","HL"]` holding `{ "atLeast": 1, "of": 3 }`, so the bullish
448
+ read may sit on the bars before the break.
449
+
261
450
  ## Entry
262
451
 
263
452
  `{ trigger, levelOffsetAtrMultiple, validForBars }` — the axis's keys, and the trigger decides
@@ -385,8 +574,8 @@ RR 2, BE 1R, trail 1.2R/giveback 35 · swing trend (4h) 1.0–2.5 ATR, RR 2, BE
385
574
 
386
575
  The one place a claim that the catalog LACKS something may live, and every row names the key it
387
576
  denies so the claim can be checked. A claim about the grammar's shape (a clause compares one
388
- column against a literal; `distance` rejects an `offset`) belongs in prose above — those are
389
- permanent. A claim that a metric is absent belongs here, or nowhere.
577
+ column with a literal or with its own previous completed bar, never with another column; `distance`
578
+ rejects an `offset`) belongs in prose above — those are permanent. A claim that a metric is absent belongs here, or nowhere.
390
579
 
391
580
  | Script / primitive | Key the catalog would need | Nearest expressible neighbour |
392
581
  |---|---|---|
@@ -87,13 +87,13 @@ would be ambiguous. Conditions: `HTF_UP` building block (`MAalign_htf is "bullis
87
87
 
88
88
  Platform `includePerpSpotFlow` (`perpSpotFlow` vocab: confirmed_bull / confirmed_bear /
89
89
  perp_led_fragile / spot_led_accumulation / neutral), `includeStructureZones`
90
- (`zones_htf_support_dist` signed %, support below price is negative; `_age_h`), custom `Tape`
90
+ (`zones_htf_support_dist` signed % of price from the zone midpoint, positive above a support zone; `_age_h`), custom `Tape`
91
91
  (`BUY_PRESSURE value`→`buyPres` 0–1, `RVOL value`, `CLOSE_CHANGE value`). Conditions:
92
92
  `SPOT_ACCUM` (`perpSpotFlow is "spot_led_accumulation"`); `NEAR_SUPPORT`
93
- (`zones_htf_support_dist between -2 0` + `zones_htf_support_age_h gte 12`); `DIP_BID` verdict
93
+ (`zones_htf_support_dist between 0 2` + `zones_htf_support_age_h gte 12`); `DIP_BID` verdict
94
94
  UP — refs plus an `N_OF(2)` quorum over `buyPres gte 0.55` / `RVOL gte 1.2` / `closeChg gt 0`
95
95
  (quorum beats a brittle ALL); `FRAGILE_POP` verdict DOWN (`perp_led_fragile` +
96
- `zones_htf_resist_dist between 0 2` + `RVOL gte 1.3`). Rules:
96
+ `zones_htf_resist_dist between -2 0` + `RVOL gte 1.3`). Rules:
97
97
  `flow_perp_spot_bull_divergence`/`_bear_divergence` 3 required · `cvd_bull_divergence`/
98
98
  `_bear_divergence` 2 · `sr_at_support`/`_at_resistance` 2 · `structure_ob_approach` 1 ·
99
99
  `volume_surge` 1. Gates 0.55 / 1 / 0.4. Levels 0.5–1.2 ATR, RR 1.5. PM: BE 0.7R; trail 0.9R
@@ -12,6 +12,7 @@ engine) and names its substitutions where the catalog lacks a primitive.
12
12
  - VWAP reversion
13
13
  - Donchian / Turtle breakout
14
14
  - ICT / SMC (FVG + order blocks)
15
+ - Pivot highs / lows, HH-HL structure and break of structure
15
16
  - Now native, formerly substituted
16
17
 
17
18
  ## TradingView ports — familiar processes, studio vocabulary
@@ -40,8 +41,9 @@ rules about the columns themselves, not about porting — they live in the skill
40
41
  distance to the plotted stop. The plotted trailing line is still best executed by the studio's
41
42
  own stop engine — `trailingTriggerR: 0` (trail from entry), giveback ~30–40 (tight factor) or
42
43
  45–55 (loose/chandelier) — because a trailing stop is a position-management mechanism, not a
43
- column. `ST_DIR` is a persisting state and not a flip event — see `## Conditions` in the
44
- skill body — so the FLIP needs an event column beside it. Name that as a substitution.
44
+ column. `ST_DIR` is a persisting state, so its FLIP is read with `enters`, natively:
45
+ `ST_DIR enters "bullish"` is TRUE on the one completed bar the direction turned, and
46
+ `ST_DIR exits "bullish"` on the bar it turned back — see `## Conditions` in the skill body.
45
47
  - **MACD + 200 MA filter** → `ABOVE_200` building block (`dist_SMA200 gt 0`) referenced by a
46
48
  carrier with `MACD_cross is "Bullish"`; rules `macd_bull/bear_cross` 3 required +
47
49
  `ma_sma200_above/below` 2 required; swing-trend geometry.
@@ -68,16 +70,27 @@ rules about the columns themselves, not about porting — they live in the skill
68
70
  and take `{rel: 'anchor'}` — see `## Header grammar` in the skill body.
69
71
  - **ICT / SMC (FVG + order blocks)** → `STRUCT_ZONES` is the native zone engine:
70
72
  `zones_htf_support_type` (`bullish FVG`/`bullish order block`), `zones_htf_support_dist
71
- between -1.5 0`, `_age_h gte 12`, HTF bias required via `MAalign_htf`; rules
73
+ between 0 1.5`, `_age_h gte 12`, HTF bias required via `MAalign_htf`; rules
72
74
  `structure_fvg_approach`/`structure_ob_approach` required (their `proximityPct` is the
73
75
  in-zone dial). Liquidity sweeps, displacement, killzones and event *sequencing* are shapes the
74
- grammar does not have — a clause compares one column against a literal, so an ordered sequence
75
- of events cannot be stated at all. That is a grammar limit, not a missing metric; name it as
76
+ grammar does not have — a clause compares one column with a literal or with its own previous
77
+ completed bar, never with another column or an earlier event, so an ordered sequence of events
78
+ cannot be stated at all. That is a grammar limit, not a missing metric; name it as
76
79
  one.
80
+ - **Pivot highs / lows, HH-HL structure and break of structure** (`ta.pivothigh(2, 2)` /
81
+ `ta.pivotlow(2, 2)`) → `SWING_FRACTAL_HIGH` / `SWING_FRACTAL_LOW` are the native swing levels and
82
+ `SWING_FRACTAL_LABEL` the structure label (`HH`/`HL`/`LH`/`LL`/`none`) — see `## Swing structure` in
83
+ the skill body. Pine's tie rule holds (an equal extreme on the left is allowed, on the right it is
84
+ not), and each level is the stepped, non-repainting series — held from the bar that completes the
85
+ pivot's right side, not plotted back on the pivot bar. Break of structure is
86
+ `dist_fractalHi crossesAbove 0` / `dist_fractalLo crossesBelow 0`; "structure is bullish" is
87
+ `fractalLabel in ["HH","HL"]`. Only two bars each side is native: a `ta.pivothigh(5, 5)` or longer
88
+ script ports onto the two-bar swing, and the spec-lock question says so.
77
89
  - **Now native, formerly substituted** — WaveTrend (`WT1`/`WT2`), QQE (`QQE_RSI_MA`/`QQE_STOP`),
78
90
  Hull (`HMA20`), Ichimoku (`ICHI_CONV`/`ICHI_BASE`/`ICHI_SPAN_A`/`ICHI_SPAN_B`/`ICHI_LAG`),
79
91
  Parabolic SAR (`PSAR`), Keltner (`KC_UPPER`/`KC_MID`/`KC_LOWER`), daily pivots
80
92
  (`PIVOT_P`/`PIVOT_R1`–`R3`/`PIVOT_S1`–`S3`), Williams %R (`WILLR14`), Stochastic RSI
81
93
  (`STOCH_RSI14`), the TTM squeeze (`KC_SQUEEZE`), Connors RSI-2 (`RSI2`), the 9/21/50 EMAs
82
- (`EMA9`/`EMA21`/`EMA50`), and literal previous-session levels (`PDH`/`PDL`/`PDO`). Port these directly —
94
+ (`EMA9`/`EMA21`/`EMA50`), literal previous-session levels (`PDH`/`PDL`/`PDO`), and fractal swing
95
+ structure (`SWING_FRACTAL_HIGH`/`SWING_FRACTAL_LOW`/`SWING_FRACTAL_LABEL`). Port these directly —
83
96
  do not offer a substitute for a primitive the catalog serves.
@@ -38,10 +38,10 @@ Not "what were its returns" — whether it did *the thing it was told to do*.
38
38
  `exitR`, and `giveBackR` (what it surrendered between its peak and the close). `exitEfficiency` is
39
39
  a union — branch on its `state` before reading `efficiencyPercent`. All of them are gross of
40
40
  costs; `netPnl` is the after-fee figure.
41
- - The record around the trades: `get_agent_game_history` and `get_user_agent_game_history` (Market
42
- Grid results, one agent or all), `get_agent_thought_log` and `get_user_thought_log` (the
43
- reasoning), `get_agent_activity_feed` and `get_user_activity_feed` (what happened), and
44
- `get_agent_explorer` for where the agent stands among public agents.
41
+ - The record around the trades: `list_my_market_grid_entries` (the player's Market Grid entries —
42
+ `agentId` for one agent's, none for every entry), `get_agent_thought_log` and
43
+ `get_user_thought_log` (the reasoning), `get_agent_activity_feed` and `get_user_activity_feed` (what
44
+ happened), and `get_agent_explorer` for where the agent stands among public agents.
45
45
  - `get_signal_performance` when the question is whether the agent's signals are working, as
46
46
  distinct from whether its trades made money.
47
47