@senpi/runtime 2.0.2-dev.openclaw-upgrade-mai.20260622175851 → 3.0.0-dev.runtime-phase-2.20260623210537
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 +9 -10
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +0 -3
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +2 -6
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +56 -43
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +108 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +0 -2
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +0 -2
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/engine-client/config-mapping.d.ts +0 -7
- package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/config-mapping.js +2 -16
- package/dist/dsl/engine-client/config-mapping.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +2 -18
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +213 -302
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +0 -15
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +9 -44
- package/dist/dsl/monitor/create-monitor.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +1 -24
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +1 -16
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +4 -35
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/dsl/plugin/options.d.ts +0 -18
- package/dist/dsl/plugin/options.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +3 -4
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +5 -14
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.js +1 -12
- package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
- package/dist/runtime/load-runtime-config.js +1 -1
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/map-strategies-registration.js +8 -7
- package/dist/runtime/map-strategies-registration.js.map +1 -1
- package/dist/runtime/map-strategies.d.ts.map +1 -1
- package/dist/runtime/map-strategies.js +3 -12
- package/dist/runtime/map-strategies.js.map +1 -1
- package/dist/runtime/reconcile-state.d.ts +0 -44
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +44 -80
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/risk-guard.d.ts.map +1 -1
- package/dist/runtime/risk-guard.js +12 -17
- package/dist/runtime/risk-guard.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +995 -2855
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +40 -133
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +0 -26
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +5 -233
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scanner-registry.d.ts +1 -1
- package/dist/runtime/scanner-registry.d.ts.map +1 -1
- package/dist/runtime/scanner-registry.js +12 -3
- package/dist/runtime/scanner-registry.js.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts +3 -3
- package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js +3 -3
- package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts +3 -3
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +36 -28
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/liveness-store.d.ts +6 -2
- package/dist/runtime-api/intake/liveness-store.d.ts.map +1 -1
- package/dist/runtime-api/intake/liveness-store.js +6 -2
- package/dist/runtime-api/intake/liveness-store.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +0 -0
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts +29 -24
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +3 -24
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +0 -6
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.d.ts +6 -0
- package/dist/runtime-api/routes/signals.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.js +14 -0
- package/dist/runtime-api/routes/signals.js.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +0 -5
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/runtime-api/server.d.ts.map +1 -1
- package/dist/runtime-api/server.js +1 -0
- package/dist/runtime-api/server.js.map +1 -1
- package/dist/scanners/external/liquidation-watchdog/README.md +107 -0
- package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +48 -0
- package/dist/scanners/external/liquidation-watchdog/producer.mjs +336 -0
- package/dist/scanners/external/momentum/README.md +148 -0
- package/dist/scanners/external/momentum/momentum.yaml +116 -0
- package/dist/scanners/external/momentum/producer.mjs +251 -0
- package/dist/scanners/external/scaffold/__main__.py +24 -65
- package/dist/scanners/external/scaffold/delivery.py +22 -56
- package/dist/scanners/external/scaffold/entrypoint_loader.py +48 -42
- package/dist/scanners/external/scaffold/journal.py +22 -15
- package/dist/scanners/external/scaffold/scaffold.py +128 -159
- package/dist/scanners/external/scaffold/signal_envelope.py +34 -177
- package/dist/scanners/external/shared/ingest.mjs +112 -0
- package/dist/scanners/external-scanner-receiver.d.ts +0 -6
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +1 -14
- package/dist/scanners/external-scanner-receiver.js.map +1 -1
- package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
- package/dist/scripts/run-dsl-standalone-live.js +3 -5
- package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
- package/dist/senpi/client.d.ts +0 -12
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +2 -31
- package/dist/senpi/client.js.map +1 -1
- package/dist/state/state-manager.d.ts +0 -17
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +0 -28
- package/dist/state/state-manager.js.map +1 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/strategy/strategy-state.js +1 -7
- package/dist/strategy/strategy-state.js.map +1 -1
- package/dist/tracking/constants.d.ts.map +1 -1
- package/dist/tracking/constants.js +0 -2
- package/dist/tracking/constants.js.map +1 -1
- package/dist/types/dsl/index.d.ts +1 -1
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/types/scanner.d.ts +0 -7
- package/dist/types/scanner.d.ts.map +1 -1
- package/examples/README.md +89 -0
- package/examples/scanners-consumer-quickstart.mjs +134 -0
- package/examples/scanners-emerging-movers-v4.mjs +287 -0
- package/examples/scanners-liquidation-watchdog.mjs +341 -0
- package/examples/scanners-live-mcp.mjs +208 -0
- package/examples/scanners-multi-strategy.mjs +165 -0
- package/examples/scanners-position-tracker-e2e.mjs +225 -0
- package/examples/scanners-scheduled-events.mjs +96 -0
- package/examples/scanners-senpi-provider-client.mjs +182 -0
- package/examples/scanners-single-strategy.mjs +198 -0
- package/examples/strategies/README.md +12 -17
- package/examples/strategies/dsl-showcase.yaml +68 -0
- package/examples/strategies/dsl.yaml +51 -0
- package/examples/strategies/fox-minimal-loosened.yaml +240 -0
- package/examples/strategies/fox-minimal.yaml +242 -0
- package/examples/strategies/minimal.yaml +46 -0
- package/examples/strategies/momentum/how-it-works.md +48 -0
- package/examples/strategies/momentum/momentum-guarded.yaml +161 -0
- package/examples/strategies/risk-showcase.yaml +93 -0
- package/examples/strategies/viper.yaml +67 -0
- package/examples/strategies/wolf.yaml +72 -0
- package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +123 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/runtime/reconcile-decide.d.ts +0 -73
- package/dist/runtime/reconcile-decide.d.ts.map +0 -1
- package/dist/runtime/reconcile-decide.js +0 -166
- package/dist/runtime/reconcile-decide.js.map +0 -1
- package/dist/runtime/reconcile-run.d.ts +0 -62
- package/dist/runtime/reconcile-run.d.ts.map +0 -1
- package/dist/runtime/reconcile-run.js +0 -173
- package/dist/runtime/reconcile-run.js.map +0 -1
- package/dist/scanners/external/scaffold/ctx.py +0 -78
- package/dist/scanners/external/scaffold/requirements.txt +0 -11
- package/dist/scanners/external/scaffold/senpi_mcp.py +0 -250
- package/dist/scanners/external/scaffold/state_manager.py +0 -175
- package/dist/utils/keyed-mutex.d.ts +0 -37
- package/dist/utils/keyed-mutex.d.ts.map +0 -1
- package/dist/utils/keyed-mutex.js +0 -77
- package/dist/utils/keyed-mutex.js.map +0 -1
- package/examples/strategies/iguana/README.md +0 -135
- package/examples/strategies/iguana/recipe.yaml +0 -137
- package/examples/strategies/iguana/scanners/scan.py +0 -243
- package/examples/strategies/iguana/scanners/scoring.py +0 -152
- package/examples/strategies/raptor/README.md +0 -156
- package/examples/strategies/raptor/recipe.yaml +0 -154
- package/examples/strategies/raptor/scanners/scan.py +0 -349
- package/examples/strategies/raptor/scanners/scoring.py +0 -452
- package/examples/strategies/spider/README.md +0 -97
- package/examples/strategies/spider/scalp/recipe.yaml +0 -129
- package/examples/strategies/spider/scalp/scanners/scan.py +0 -219
- package/examples/strategies/spider/scalp/scanners/scoring.py +0 -179
- package/examples/strategies/spider/swing/recipe.yaml +0 -141
- package/examples/strategies/spider/swing/scanners/scan.py +0 -334
- package/examples/strategies/spider/swing/scanners/scoring.py +0 -184
- package/examples/strategies/turbine/README.md +0 -167
- package/examples/strategies/turbine/recipe-runners.yaml +0 -123
- package/examples/strategies/turbine/recipe-volume.yaml +0 -129
- package/examples/strategies/turbine/scanners/scan.py +0 -283
- package/examples/strategies/turbine/scanners/scoring.py +0 -190
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External momentum producer.
|
|
3
|
+
*
|
|
4
|
+
* A standalone momentum scanner that:
|
|
5
|
+
* 1. Pulls instruments + latest candles from the live Senpi MCP server.
|
|
6
|
+
* 2. Computes last-vs-prev close % move against configurable thresholds.
|
|
7
|
+
* 3. Pushes MOMENTUM_BREAKOUT signals into a running runtime via the
|
|
8
|
+
* `openclaw senpi external-scanner ingest` CLI command.
|
|
9
|
+
*
|
|
10
|
+
* Prereqs:
|
|
11
|
+
* - `npm run build` so dist/ is current (used only for the MCP provider bridge).
|
|
12
|
+
* - A runtime installed from ./momentum.yaml is running for STRATEGY_ADDRESS.
|
|
13
|
+
* - `openclaw` CLI on PATH and authenticated against that runtime.
|
|
14
|
+
* - SENPI_API_KEY and SENPI_MCP_URL env vars set.
|
|
15
|
+
*
|
|
16
|
+
* Run: node src/scanners/external/momentum/producer.mjs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
20
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
21
|
+
import { createSenpiScannerProviders } from "../../../../dist/scanners/index.js";
|
|
22
|
+
import { pushScannerSignals } from "../shared/ingest.mjs";
|
|
23
|
+
|
|
24
|
+
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const SENPI_API_KEY = process.env.SENPI_API_KEY;
|
|
27
|
+
const SENPI_MCP_URL = process.env.SENPI_MCP_URL;
|
|
28
|
+
const STRATEGY_ADDRESS = process.env.STRATEGY_ADDRESS;
|
|
29
|
+
const SCANNER_NAME =
|
|
30
|
+
process.env.EXTERNAL_SCANNER_NAME ?? "external_momentum";
|
|
31
|
+
const OPENCLAW_BIN = process.env.OPENCLAW_BIN ?? "openclaw";
|
|
32
|
+
const SCANNER_ID = "external-momentum-producer";
|
|
33
|
+
|
|
34
|
+
const config = {
|
|
35
|
+
timeframe: process.env.TIMEFRAME ?? "1h",
|
|
36
|
+
minMovePct: numEnv("MIN_MOVE_PCT", 1.5),
|
|
37
|
+
minDayVolume: numEnv("MIN_DAY_VOLUME", 1_000_000),
|
|
38
|
+
minLeverage: numEnv("MIN_LEVERAGE", 5),
|
|
39
|
+
maxCandidates: numEnv("MAX_CANDIDATES", 20),
|
|
40
|
+
minSignalScore: numEnv("MIN_SIGNAL_SCORE", 0.2),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (!SENPI_API_KEY || !SENPI_MCP_URL || !STRATEGY_ADDRESS) {
|
|
44
|
+
console.error(
|
|
45
|
+
"Missing required env vars. Set SENPI_API_KEY, SENPI_MCP_URL, and STRATEGY_ADDRESS.\n" +
|
|
46
|
+
" export SENPI_API_KEY='...'\n" +
|
|
47
|
+
" export SENPI_MCP_URL='https://mcp.prod.senpi.ai/mcp'\n" +
|
|
48
|
+
" export STRATEGY_ADDRESS='0x...'",
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function numEnv(name, fallback) {
|
|
54
|
+
const raw = process.env[name];
|
|
55
|
+
if (raw === undefined || raw === "") return fallback;
|
|
56
|
+
const parsed = Number(raw);
|
|
57
|
+
if (!Number.isFinite(parsed)) {
|
|
58
|
+
throw new Error(`Env ${name} must be a finite number, got "${raw}"`);
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── MCP → SenpiToolClient bridge ───────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function createSenpiToolClient(mcpClient) {
|
|
66
|
+
return {
|
|
67
|
+
async callTool(name, args) {
|
|
68
|
+
const result = await mcpClient.callTool({ name, arguments: args });
|
|
69
|
+
|
|
70
|
+
if (result.isError) {
|
|
71
|
+
const errorText =
|
|
72
|
+
Array.isArray(result.content) &&
|
|
73
|
+
result.content.length > 0 &&
|
|
74
|
+
result.content[0].type === "text"
|
|
75
|
+
? result.content[0].text
|
|
76
|
+
: JSON.stringify(result.content);
|
|
77
|
+
throw new Error(`MCP tool "${name}" returned error: ${errorText}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (result.structuredContent !== undefined) {
|
|
81
|
+
return result.structuredContent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(result.content) && result.content.length > 0) {
|
|
85
|
+
const first = result.content[0];
|
|
86
|
+
if (first.type === "text") {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(first.text);
|
|
89
|
+
} catch {
|
|
90
|
+
return first.text;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result.content;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Momentum logic ─────────────────────────────────────────────────────────
|
|
101
|
+
// Self-contained port of src/scanners/implementations/momentum.ts. Kept free
|
|
102
|
+
// of any runtime-internal imports.
|
|
103
|
+
|
|
104
|
+
function clamp01(value) {
|
|
105
|
+
if (!Number.isFinite(value)) return 0;
|
|
106
|
+
if (value < 0) return 0;
|
|
107
|
+
if (value > 1) return 1;
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function selectCandidates(instruments, cfg) {
|
|
112
|
+
const eligible = instruments.filter((inst) => {
|
|
113
|
+
if (inst.isDelisted) return false;
|
|
114
|
+
if ((inst.maxLeverage ?? 0) < cfg.minLeverage) return false;
|
|
115
|
+
const vol = inst.context?.dayNtlVlm ?? 0;
|
|
116
|
+
return vol >= cfg.minDayVolume;
|
|
117
|
+
});
|
|
118
|
+
eligible.sort(
|
|
119
|
+
(a, b) => (b.context?.dayNtlVlm ?? 0) - (a.context?.dayNtlVlm ?? 0),
|
|
120
|
+
);
|
|
121
|
+
return eligible.slice(0, cfg.maxCandidates);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function computeMovePct(series) {
|
|
125
|
+
if (!series || series.length < 2) return null;
|
|
126
|
+
const prev = series[series.length - 2]?.close;
|
|
127
|
+
const last = series[series.length - 1]?.close;
|
|
128
|
+
if (!Number.isFinite(prev) || !Number.isFinite(last) || prev <= 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return { prev, last, movePct: ((last - prev) / prev) * 100 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildSignal({ instrument, move, timeframe, now }) {
|
|
135
|
+
const movePct = move.movePct;
|
|
136
|
+
const absMove = Math.abs(movePct);
|
|
137
|
+
const direction = movePct > 0 ? "LONG" : "SHORT";
|
|
138
|
+
// Score scales from 0 at minMovePct to 1 at 5% move, clamped.
|
|
139
|
+
const score = clamp01((absMove - config.minMovePct) / (5 - config.minMovePct));
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
scannerId: SCANNER_ID,
|
|
143
|
+
signalType: "MOMENTUM_BREAKOUT",
|
|
144
|
+
asset: instrument.name,
|
|
145
|
+
direction,
|
|
146
|
+
score,
|
|
147
|
+
timestamp: now,
|
|
148
|
+
factors: {
|
|
149
|
+
breakout: true,
|
|
150
|
+
large_move: absMove >= 3,
|
|
151
|
+
},
|
|
152
|
+
meta: {
|
|
153
|
+
timeframe,
|
|
154
|
+
movePct,
|
|
155
|
+
prev: move.prev,
|
|
156
|
+
last: move.last,
|
|
157
|
+
dayNtlVlm: instrument.context?.dayNtlVlm ?? 0,
|
|
158
|
+
maxLeverage: instrument.maxLeverage,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
console.log(`External momentum producer`);
|
|
167
|
+
console.log(` MCP: ${SENPI_MCP_URL}`);
|
|
168
|
+
console.log(` address: ${STRATEGY_ADDRESS}`);
|
|
169
|
+
console.log(` scanner: ${SCANNER_NAME}`);
|
|
170
|
+
console.log(` config: ${JSON.stringify(config)}`);
|
|
171
|
+
|
|
172
|
+
const transport = new StreamableHTTPClientTransport(new URL(SENPI_MCP_URL), {
|
|
173
|
+
requestInit: { headers: { Authorization: `Bearer ${SENPI_API_KEY}` } },
|
|
174
|
+
});
|
|
175
|
+
const mcpClient = new Client({
|
|
176
|
+
name: "external-momentum",
|
|
177
|
+
version: "0.0.1",
|
|
178
|
+
});
|
|
179
|
+
await mcpClient.connect(transport);
|
|
180
|
+
|
|
181
|
+
const providers = createSenpiScannerProviders({
|
|
182
|
+
client: createSenpiToolClient(mcpClient),
|
|
183
|
+
logLevel: "warn",
|
|
184
|
+
cacheTtlMs: { instruments: 0, smData: 0, candles: 0 },
|
|
185
|
+
candleIntervals: [config.timeframe],
|
|
186
|
+
maxCandleAssets: config.maxCandidates,
|
|
187
|
+
retries: 2,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const instruments = await providers.getInstruments(STRATEGY_ADDRESS);
|
|
192
|
+
const candidates = selectCandidates(instruments, config);
|
|
193
|
+
console.log(
|
|
194
|
+
`\nInstruments: ${instruments.length}, candidates: ${candidates.length}`,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (candidates.length === 0) {
|
|
198
|
+
console.log("No candidates after filtering — skipping ingest.");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const candles = await providers.getCandles(STRATEGY_ADDRESS, candidates, {
|
|
203
|
+
scannerId: SCANNER_ID,
|
|
204
|
+
preferredAssets: candidates.map((c) => c.name),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
const signals = [];
|
|
209
|
+
for (const inst of candidates) {
|
|
210
|
+
const series = candles[inst.name]?.candles?.[config.timeframe];
|
|
211
|
+
const move = computeMovePct(series);
|
|
212
|
+
if (!move) continue;
|
|
213
|
+
if (Math.abs(move.movePct) < config.minMovePct) continue;
|
|
214
|
+
const signal = buildSignal({
|
|
215
|
+
instrument: inst,
|
|
216
|
+
move,
|
|
217
|
+
timeframe: config.timeframe,
|
|
218
|
+
now,
|
|
219
|
+
});
|
|
220
|
+
if (signal.score < config.minSignalScore) continue;
|
|
221
|
+
signals.push(signal);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`\nSignals: ${signals.length}`);
|
|
225
|
+
for (const s of signals) {
|
|
226
|
+
console.log(
|
|
227
|
+
` ${s.asset} ${s.direction} move=${s.meta.movePct.toFixed(2)}% score=${s.score.toFixed(2)}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (signals.length === 0) {
|
|
232
|
+
console.log("\nNothing to ingest — skipping gateway call.");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const response = await pushScannerSignals({
|
|
237
|
+
address: STRATEGY_ADDRESS,
|
|
238
|
+
scanner: SCANNER_NAME,
|
|
239
|
+
signals,
|
|
240
|
+
openclawBin: OPENCLAW_BIN,
|
|
241
|
+
});
|
|
242
|
+
console.log("\nIngest response:", JSON.stringify(response));
|
|
243
|
+
} finally {
|
|
244
|
+
await transport.close();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
main().catch((error) => {
|
|
249
|
+
console.error("\nProducer failed:", error.message ?? error);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
"""Process entry for the scaffold core
|
|
1
|
+
"""Process entry for the scaffold core.
|
|
2
2
|
|
|
3
3
|
Usage: python3 -m scaffold <launch-config.json>
|
|
4
4
|
|
|
5
|
-
The launch config JSON
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
wallet — strategy wallet (-> ctx.wallet).
|
|
16
|
-
scanner_name — this scanner's recipe name (-> ctx.scanner_name).
|
|
17
|
-
|
|
18
|
-
Optional keys:
|
|
19
|
-
timeout_seconds — per-tick wall-clock budget (default: interval_seconds).
|
|
20
|
-
state_path — JSON state file path (None => no persistence).
|
|
21
|
-
inputs — tunables dict for scan() (was `params`); default {}.
|
|
22
|
-
state_history_max_count — state series bound; default 0 = history disabled.
|
|
5
|
+
The launch config JSON must contain:
|
|
6
|
+
path — directory added to sys.path (the scanner's working directory).
|
|
7
|
+
entrypoint — module name to import from `path`.
|
|
8
|
+
interval — seconds between tick starts.
|
|
9
|
+
|
|
10
|
+
Optional fields:
|
|
11
|
+
timeout_seconds — per-tick wall-clock budget (default: interval).
|
|
12
|
+
params — tunables dict passed to scan() as the first argument.
|
|
13
|
+
state_path — path to the JSON state file.
|
|
14
|
+
max_ticks — cap on tick count (for tests).
|
|
23
15
|
|
|
24
16
|
Delivery sink is wired when EXTERNAL_SCANNER_ID and SIGNAL_INGESTION_URL are set in
|
|
25
17
|
the environment. If either is absent the scaffold runs with the default no-op sink
|
|
@@ -31,18 +23,15 @@ import os
|
|
|
31
23
|
import sys
|
|
32
24
|
|
|
33
25
|
|
|
34
|
-
def _main(
|
|
35
|
-
if argv
|
|
36
|
-
argv = sys.argv
|
|
37
|
-
|
|
38
|
-
if len(argv) < 2:
|
|
26
|
+
def _main() -> int:
|
|
27
|
+
if len(sys.argv) < 2:
|
|
39
28
|
print(
|
|
40
29
|
"Usage: python3 -m scaffold <launch-config.json>",
|
|
41
30
|
file=sys.stderr,
|
|
42
31
|
)
|
|
43
32
|
return 1
|
|
44
33
|
|
|
45
|
-
config_path = argv[1]
|
|
34
|
+
config_path = sys.argv[1]
|
|
46
35
|
try:
|
|
47
36
|
with open(config_path, "r", encoding="utf-8") as fh:
|
|
48
37
|
config = json.load(fh)
|
|
@@ -50,43 +39,22 @@ def _main(argv=None) -> int:
|
|
|
50
39
|
print(f"[scaffold] launch config error: {exc}", file=sys.stderr)
|
|
51
40
|
return 1
|
|
52
41
|
|
|
53
|
-
# ---- Required §4a keys: missing => fail loud (no magic default) ----
|
|
54
42
|
try:
|
|
55
43
|
path = config["path"]
|
|
56
44
|
entrypoint = config["entrypoint"]
|
|
57
|
-
interval = float(config["
|
|
58
|
-
default_signal_validity_seconds = int(config["default_signal_validity_seconds"])
|
|
59
|
-
wallet = config["wallet"]
|
|
60
|
-
scanner_name = config["scanner_name"]
|
|
45
|
+
interval = float(config["interval"])
|
|
61
46
|
except (KeyError, TypeError, ValueError) as exc:
|
|
62
|
-
print(
|
|
63
|
-
f"[scaffold] launch config missing/invalid required field: {exc}",
|
|
64
|
-
file=sys.stderr,
|
|
65
|
-
)
|
|
47
|
+
print(f"[scaffold] launch config missing required field: {exc}", file=sys.stderr)
|
|
66
48
|
return 1
|
|
67
49
|
|
|
68
|
-
# ---- Optional §4a keys ----
|
|
69
50
|
timeout_seconds = config.get("timeout_seconds")
|
|
70
|
-
|
|
71
|
-
inputs = config.get("inputs") or {}
|
|
51
|
+
params = config.get("params") or {}
|
|
72
52
|
state_path = config.get("state_path")
|
|
73
|
-
state_history_max_count = config.get("state_history_max_count") or 0
|
|
74
|
-
# Per-signal data{} schema (§4a). None / absent => no scaffold-side validation
|
|
75
|
-
# (intake still validates authoritatively). A dict turns on early, loud
|
|
76
|
-
# scaffold-side validation that POSTs a violation to /errors.
|
|
77
|
-
signal_data_schema = config.get("signal_data_schema")
|
|
78
|
-
if signal_data_schema is not None and not isinstance(signal_data_schema, dict):
|
|
79
|
-
signal_data_schema = None
|
|
80
53
|
max_ticks = config.get("max_ticks")
|
|
81
54
|
|
|
82
|
-
|
|
83
|
-
from scaffold import ScanEntrypointError, ScanSignatureError
|
|
55
|
+
from scaffold import ScanEntrypointError, ScanSignatureError, run_scaffold
|
|
84
56
|
from scaffold.loop_primitives import log_event
|
|
85
57
|
|
|
86
|
-
# Resolve run_scaffold via the package symbol so a test stub patched onto the
|
|
87
|
-
# package (or this module) is picked up.
|
|
88
|
-
run_scaffold = globals().get("run_scaffold") or scaffold_pkg.run_scaffold
|
|
89
|
-
|
|
90
58
|
# Build the delivery sink when scanner_id and intake URL are available.
|
|
91
59
|
sink = None
|
|
92
60
|
scanner_id = os.environ.get("EXTERNAL_SCANNER_ID", "").strip()
|
|
@@ -99,24 +67,19 @@ def _main(argv=None) -> int:
|
|
|
99
67
|
post_timeout = float(os.environ.get("SIGNAL_POST_TIMEOUT_SECONDS", "5"))
|
|
100
68
|
|
|
101
69
|
# Derive journal path from the state file directory when available.
|
|
102
|
-
journal_path = None
|
|
70
|
+
journal_path: str | None = None
|
|
103
71
|
if state_path:
|
|
104
|
-
|
|
105
|
-
|
|
72
|
+
import os as _os
|
|
73
|
+
journal_path = _os.path.join(
|
|
74
|
+
_os.path.dirname(_os.path.abspath(state_path)),
|
|
106
75
|
"delivery-journal.jsonl",
|
|
107
76
|
)
|
|
108
77
|
|
|
109
|
-
# §4a threads the per-data-key schema (`signal_data_schema`) to the
|
|
110
|
-
# scaffold so the envelope validates each signal's `data` scaffold-side
|
|
111
|
-
# and POSTs a violation to /errors (early, loud feedback). None ⇒ no
|
|
112
|
-
# scaffold-side validation. The intake still performs authoritative
|
|
113
|
-
# server-side schema validation regardless (this is additive).
|
|
114
78
|
sink = build_delivery_sink(
|
|
115
79
|
scanner_id=scanner_id,
|
|
116
80
|
intake_url=intake_url,
|
|
81
|
+
interval=interval,
|
|
117
82
|
journal_path=journal_path,
|
|
118
|
-
signal_data_schema=signal_data_schema,
|
|
119
|
-
default_signal_validity_seconds=default_signal_validity_seconds,
|
|
120
83
|
timeout=post_timeout,
|
|
121
84
|
)
|
|
122
85
|
log_event("delivery_sink_enabled", scanner_id=scanner_id, intake_url=intake_url)
|
|
@@ -135,12 +98,8 @@ def _main(argv=None) -> int:
|
|
|
135
98
|
entrypoint,
|
|
136
99
|
interval=interval,
|
|
137
100
|
timeout_seconds=timeout_seconds,
|
|
138
|
-
|
|
101
|
+
params=params,
|
|
139
102
|
state_path=state_path,
|
|
140
|
-
scanner_name=scanner_name,
|
|
141
|
-
wallet=wallet,
|
|
142
|
-
default_signal_validity_seconds=default_signal_validity_seconds,
|
|
143
|
-
state_history_max_count=state_history_max_count,
|
|
144
103
|
sink=sink,
|
|
145
104
|
max_ticks=max_ticks,
|
|
146
105
|
)
|
|
@@ -4,9 +4,8 @@ build_delivery_sink(
|
|
|
4
4
|
*,
|
|
5
5
|
scanner_id: str,
|
|
6
6
|
intake_url: str,
|
|
7
|
+
interval: float, # seconds; valid_until default = produced_at + 2*interval
|
|
7
8
|
journal_path: str | None,
|
|
8
|
-
signal_data_schema: dict | None, # per-data-key {type, required?}; None = no validation
|
|
9
|
-
default_signal_validity_seconds: int, # fallback signal TTL (seconds)
|
|
10
9
|
timeout: float = 5.0,
|
|
11
10
|
clock: Callable[[], float] = time.time,
|
|
12
11
|
) -> sink
|
|
@@ -33,7 +32,6 @@ from __future__ import annotations
|
|
|
33
32
|
|
|
34
33
|
import sys
|
|
35
34
|
import time
|
|
36
|
-
import uuid
|
|
37
35
|
from typing import Any, Callable
|
|
38
36
|
|
|
39
37
|
from .http_post import ERRORS_PATH, SIGNALS_PATH, TransportError, post_json
|
|
@@ -49,14 +47,14 @@ def build_delivery_sink(
|
|
|
49
47
|
*,
|
|
50
48
|
scanner_id: str,
|
|
51
49
|
intake_url: str,
|
|
50
|
+
interval: float,
|
|
52
51
|
journal_path: str | None,
|
|
53
|
-
signal_data_schema: dict | None,
|
|
54
|
-
default_signal_validity_seconds: int,
|
|
55
52
|
timeout: float = 5.0,
|
|
56
53
|
clock: Callable[[], float] = time.time,
|
|
57
54
|
):
|
|
58
55
|
"""Factory: build and return the delivery sink callable."""
|
|
59
56
|
|
|
57
|
+
interval_ms = int(interval * 1000)
|
|
60
58
|
journal = Journal(journal_path)
|
|
61
59
|
replayed = [False] # mutable cell; replay runs once on first invocation
|
|
62
60
|
|
|
@@ -89,35 +87,16 @@ def build_delivery_sink(
|
|
|
89
87
|
log_event("delivery_dropped_expired", entry_id=entry.id, asset=entry.asset)
|
|
90
88
|
return to_send
|
|
91
89
|
|
|
92
|
-
def _on_reject(reason: dict) -> None:
|
|
93
|
-
"""Loud rejection: log AND POST to /errors (design §4 — never a silent drop)."""
|
|
94
|
-
log_event("delivery_candidate_invalid", **reason)
|
|
95
|
-
_post_error({"type": "candidate_rejected", **reason})
|
|
96
|
-
|
|
97
90
|
def _wrap_candidates(candidates, produced_at_ms: int) -> list[tuple]:
|
|
98
|
-
"""Wrap candidates into (signal, entry_id) pairs, skipping invalid ones.
|
|
99
|
-
|
|
100
|
-
Schema-rejected candidates are NOT silently dropped: candidate_to_signal
|
|
101
|
-
calls _on_reject, which logs AND POSTs to /errors.
|
|
102
|
-
"""
|
|
91
|
+
"""Wrap candidates into (signal, entry_id) pairs, skipping invalid ones."""
|
|
103
92
|
result = []
|
|
104
93
|
for c in candidates:
|
|
105
|
-
|
|
106
|
-
# through the envelope so the POSTed signal carries it. record_produced
|
|
107
|
-
# re-uses this same id as the entry id, so wire signal_id == journal eid.
|
|
108
|
-
eid = uuid.uuid4().hex
|
|
109
|
-
sig = candidate_to_signal(
|
|
110
|
-
c,
|
|
111
|
-
produced_at_ms,
|
|
112
|
-
signal_id=eid,
|
|
113
|
-
signal_data_schema=signal_data_schema,
|
|
114
|
-
default_signal_validity_seconds=default_signal_validity_seconds,
|
|
115
|
-
on_reject=_on_reject,
|
|
116
|
-
)
|
|
94
|
+
sig = candidate_to_signal(c, produced_at_ms, interval_ms)
|
|
117
95
|
if sig is None:
|
|
96
|
+
log_event("delivery_candidate_invalid", candidate=repr(c))
|
|
118
97
|
continue
|
|
119
|
-
|
|
120
|
-
result.append((sig,
|
|
98
|
+
eid = journal.record_produced(sig, now_ms=produced_at_ms)
|
|
99
|
+
result.append((sig, eid))
|
|
121
100
|
return result
|
|
122
101
|
|
|
123
102
|
def _post_signals(signals: list[dict]) -> tuple[int, Any]:
|
|
@@ -146,33 +125,20 @@ def build_delivery_sink(
|
|
|
146
125
|
if not isinstance(per_signal, list):
|
|
147
126
|
per_signal = []
|
|
148
127
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
log_event("delivery_status_unmatched", signal_id=eid)
|
|
164
|
-
continue
|
|
165
|
-
st = sig_status.get("status")
|
|
166
|
-
if st == "accepted":
|
|
167
|
-
journal.record_outcome(eid, "delivered", correlationId=sig_status.get("correlationId", ""))
|
|
168
|
-
elif st == "rejected":
|
|
169
|
-
journal.record_outcome(eid, "rejected", reason=sig_status.get("reason", ""))
|
|
170
|
-
elif st in ("expired", "superseded"):
|
|
171
|
-
journal.record_outcome(eid, st)
|
|
172
|
-
else:
|
|
173
|
-
# Unknown status — treat as undelivered (no outcome written)
|
|
174
|
-
log_event("delivery_unknown_status", entry_id=eid, status=st)
|
|
175
|
-
# Entries with no matching status returned stay pending (retry next tick).
|
|
128
|
+
for i, eid in enumerate(pending_eids):
|
|
129
|
+
if i < len(per_signal):
|
|
130
|
+
sig_status = per_signal[i]
|
|
131
|
+
st = sig_status.get("status") if isinstance(sig_status, dict) else None
|
|
132
|
+
if st == "accepted":
|
|
133
|
+
journal.record_outcome(eid, "delivered", correlationId=sig_status.get("correlationId", ""))
|
|
134
|
+
elif st == "rejected":
|
|
135
|
+
journal.record_outcome(eid, "rejected", reason=sig_status.get("reason", ""))
|
|
136
|
+
elif st in ("expired", "superseded"):
|
|
137
|
+
journal.record_outcome(eid, st)
|
|
138
|
+
else:
|
|
139
|
+
# Unknown status — treat as undelivered (no outcome written)
|
|
140
|
+
log_event("delivery_unknown_status", entry_id=eid, status=st)
|
|
141
|
+
# If no per-signal status returned for this entry, leave it pending (retry)
|
|
176
142
|
|
|
177
143
|
return True
|
|
178
144
|
|
|
@@ -20,7 +20,7 @@ class ScanEntrypointError(Exception):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ScanSignatureError(Exception):
|
|
23
|
-
"""scan's call shape is incompatible with
|
|
23
|
+
"""scan's call shape is incompatible with scaffold(params, state[, client])."""
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def add_path_to_sys_path(path: str) -> None:
|
|
@@ -32,22 +32,16 @@ def add_path_to_sys_path(path: str) -> None:
|
|
|
32
32
|
def import_entrypoint(path: str, entrypoint: str):
|
|
33
33
|
"""Put `path` on sys.path, then import `entrypoint` by module name.
|
|
34
34
|
|
|
35
|
-
The recipe expresses `entrypoint` as a FILENAME (e.g. `scan.py`, the shape the
|
|
36
|
-
B1 launcher writes verbatim into the launch config and the §4a entrypoint FS
|
|
37
|
-
check validates). importlib needs a MODULE name, so a trailing `.py` is
|
|
38
|
-
stripped here — the only place the filename<->module-name bridge lives.
|
|
39
|
-
|
|
40
35
|
Raises ScanEntrypointError on any import/syntax/missing-sibling failure,
|
|
41
36
|
chaining the underlying exception.
|
|
42
37
|
"""
|
|
43
38
|
add_path_to_sys_path(path)
|
|
44
|
-
module_name = entrypoint[:-3] if entrypoint.endswith(".py") else entrypoint
|
|
45
39
|
try:
|
|
46
40
|
# Force re-import when the module has been previously imported so that
|
|
47
41
|
# tests can write fresh modules to tmp_path without cache collisions.
|
|
48
|
-
if
|
|
49
|
-
del sys.modules[
|
|
50
|
-
return importlib.import_module(
|
|
42
|
+
if entrypoint in sys.modules:
|
|
43
|
+
del sys.modules[entrypoint]
|
|
44
|
+
return importlib.import_module(entrypoint)
|
|
51
45
|
except Exception as exc:
|
|
52
46
|
raise ScanEntrypointError(
|
|
53
47
|
f"Failed to import entrypoint '{entrypoint}' from '{path}': {exc}"
|
|
@@ -73,25 +67,19 @@ def locate_scan(module) -> Callable:
|
|
|
73
67
|
|
|
74
68
|
|
|
75
69
|
def verify_scan_signature(scan: Callable) -> None:
|
|
76
|
-
"""Verify scan's call shape is compatible with
|
|
77
|
-
|
|
78
|
-
v2 calls scan with EXACTLY two positionals (inputs, ctx). A signature must be
|
|
79
|
-
callable that way.
|
|
70
|
+
"""Verify scan's call shape is compatible with scan(params, state[, client]).
|
|
80
71
|
|
|
81
72
|
Accepted shapes:
|
|
82
|
-
scan(
|
|
83
|
-
scan(
|
|
84
|
-
scan(
|
|
73
|
+
scan(params, state) — 2 required
|
|
74
|
+
scan(params, state, client) — 3 required
|
|
75
|
+
scan(params, state, client=None) — 2 required, 1 optional
|
|
76
|
+
scan(params, state, *args) — variadic absorbs client
|
|
85
77
|
scan(*args, **kwargs) — fully variadic, accepts anything
|
|
86
78
|
|
|
87
79
|
Rejected shapes:
|
|
88
80
|
scan() — 0 required
|
|
89
|
-
scan(
|
|
90
|
-
scan(
|
|
91
|
-
scan(a, b, c, d) — >=4 required
|
|
92
|
-
|
|
93
|
-
The contract is now EXACTLY two positionals: a 3rd REQUIRED positional cannot
|
|
94
|
-
be satisfied by scan(inputs, ctx) and is rejected.
|
|
81
|
+
scan(params) — 1 required
|
|
82
|
+
scan(a, b, c, d) — >=4 fixed required
|
|
95
83
|
|
|
96
84
|
Raises ScanSignatureError naming the found arity on mismatch.
|
|
97
85
|
"""
|
|
@@ -106,39 +94,57 @@ def verify_scan_signature(scan: Callable) -> None:
|
|
|
106
94
|
has_var_positional = any(
|
|
107
95
|
p.kind == inspect.Parameter.VAR_POSITIONAL for p in params
|
|
108
96
|
)
|
|
97
|
+
has_var_keyword = any(
|
|
98
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in params
|
|
99
|
+
)
|
|
109
100
|
|
|
110
|
-
#
|
|
111
|
-
|
|
101
|
+
# A *args/**kwargs-only signature accepts any positional call.
|
|
102
|
+
if has_var_positional or has_var_keyword:
|
|
103
|
+
# Check that it actually accepts at least 2 positional args (i.e., no
|
|
104
|
+
# required positional params ahead of *args that block the call).
|
|
105
|
+
required_before_var = [
|
|
106
|
+
p for p in params
|
|
107
|
+
if p.kind in (
|
|
108
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
109
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
110
|
+
)
|
|
111
|
+
and p.default is inspect.Parameter.empty
|
|
112
|
+
]
|
|
113
|
+
# If there are >=4 required positionals even WITH *args, reject.
|
|
114
|
+
if len(required_before_var) >= 4:
|
|
115
|
+
raise ScanSignatureError(
|
|
116
|
+
f"scan requires {len(required_before_var)} fixed positional params; "
|
|
117
|
+
f"expected 2 or 3 (found arity: {len(required_before_var)})."
|
|
118
|
+
)
|
|
119
|
+
# Otherwise, variadic accepts our call.
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
# Count fixed positional params (POSITIONAL_ONLY or POSITIONAL_OR_KEYWORD).
|
|
123
|
+
fixed_positional = [
|
|
112
124
|
p for p in params
|
|
113
125
|
if p.kind in (
|
|
114
126
|
inspect.Parameter.POSITIONAL_ONLY,
|
|
115
127
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
116
128
|
)
|
|
117
|
-
and p.default is inspect.Parameter.empty
|
|
118
129
|
]
|
|
119
|
-
|
|
130
|
+
required_fixed = [
|
|
131
|
+
p for p in fixed_positional
|
|
132
|
+
if p.default is inspect.Parameter.empty
|
|
133
|
+
]
|
|
120
134
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if has_var_positional:
|
|
124
|
-
if n_required > 2:
|
|
125
|
-
raise ScanSignatureError(
|
|
126
|
-
f"scan requires {n_required} fixed positional params; the v2 "
|
|
127
|
-
f"contract calls scan(inputs, ctx) — exactly 2. Found arity: {n_required}."
|
|
128
|
-
)
|
|
129
|
-
return
|
|
135
|
+
n_required = len(required_fixed)
|
|
136
|
+
n_fixed = len(fixed_positional)
|
|
130
137
|
|
|
131
138
|
if n_required < 2:
|
|
132
139
|
raise ScanSignatureError(
|
|
133
|
-
f"scan has {n_required} required positional param(s);
|
|
134
|
-
f"
|
|
140
|
+
f"scan has {n_required} required positional param(s); "
|
|
141
|
+
f"expected at least 2 (params, state). Found arity: {n_required}."
|
|
135
142
|
)
|
|
136
143
|
|
|
137
|
-
if n_required
|
|
144
|
+
if n_required >= 4:
|
|
138
145
|
raise ScanSignatureError(
|
|
139
|
-
f"scan has {n_required} required positional params;
|
|
140
|
-
f"
|
|
141
|
-
f"(the old v1 client shape) is no longer accepted. Found arity: {n_required}."
|
|
146
|
+
f"scan has {n_required} required positional params; "
|
|
147
|
+
f"expected at most 3 (params, state, client). Found arity: {n_required}."
|
|
142
148
|
)
|
|
143
149
|
|
|
144
150
|
|