@senpi/runtime 1.1.0-dev.runtime-phase-2.20260609123448 → 1.1.0-dev.runtime-phase-2.20260609132603
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/actions/close-position/close-position.action.d.ts +16 -0
- package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
- package/dist/actions/close-position/close-position.action.js +299 -38
- package/dist/actions/close-position/close-position.action.js.map +1 -1
- package/dist/actions/decision-engine.d.ts +2 -0
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/llm-decision/factory.d.ts +3 -0
- package/dist/actions/llm-decision/factory.d.ts.map +1 -1
- package/dist/actions/llm-decision/factory.js +1 -0
- package/dist/actions/llm-decision/factory.js.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts +13 -0
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.js +195 -68
- package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts +12 -0
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +356 -109
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/otel-helpers.d.ts +65 -0
- package/dist/actions/otel-helpers.d.ts.map +1 -0
- package/dist/actions/otel-helpers.js +122 -0
- package/dist/actions/otel-helpers.js.map +1 -0
- package/dist/actions/position-tracker/position-tracker.action.d.ts +6 -0
- package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.js +48 -0
- package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
- package/dist/otel/context.d.ts +24 -0
- package/dist/otel/context.d.ts.map +1 -0
- package/dist/otel/context.js +29 -0
- package/dist/otel/context.js.map +1 -0
- package/dist/otel/emitter.d.ts +73 -0
- package/dist/otel/emitter.d.ts.map +1 -0
- package/dist/otel/emitter.js +248 -0
- package/dist/otel/emitter.js.map +1 -0
- package/dist/otel/encode.d.ts +48 -0
- package/dist/otel/encode.d.ts.map +1 -0
- package/dist/otel/encode.js +100 -0
- package/dist/otel/encode.js.map +1 -0
- package/dist/otel/index.d.ts +13 -0
- package/dist/otel/index.d.ts.map +1 -0
- package/dist/otel/index.js +10 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/otel/transport.d.ts +57 -0
- package/dist/otel/transport.d.ts.map +1 -0
- package/dist/otel/transport.js +208 -0
- package/dist/otel/transport.js.map +1 -0
- package/dist/otel/types.d.ts +62 -0
- package/dist/otel/types.d.ts.map +1 -0
- package/dist/otel/types.js +2 -0
- package/dist/otel/types.js.map +1 -0
- package/dist/runtime/create-actions.d.ts.map +1 -1
- package/dist/runtime/create-actions.js +2 -0
- package/dist/runtime/create-actions.js.map +1 -1
- package/dist/runtime/create-runtime-context.d.ts +2 -0
- package/dist/runtime/create-runtime-context.d.ts.map +1 -1
- package/dist/runtime/create-runtime-context.js +1 -0
- package/dist/runtime/create-runtime-context.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +1 -1
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +2 -1
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/risk-guard.d.ts +10 -0
- package/dist/runtime/risk-guard.d.ts.map +1 -1
- package/dist/runtime/risk-guard.js +6 -12
- package/dist/runtime/risk-guard.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +24 -0
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +1 -0
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +5 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +140 -4
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/strategy-lifecycle.d.ts +45 -0
- package/dist/runtime/strategy-lifecycle.d.ts.map +1 -0
- package/dist/runtime/strategy-lifecycle.js +117 -0
- package/dist/runtime/strategy-lifecycle.js.map +1 -0
- package/dist/scanners/engine/data-providers.d.ts +2 -0
- package/dist/scanners/engine/data-providers.d.ts.map +1 -1
- package/dist/scanners/engine/data-providers.js.map +1 -1
- package/dist/scanners/engine/engine.d.ts.map +1 -1
- package/dist/scanners/engine/engine.js +68 -9
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts +2 -0
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +1 -0
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/senpi/client.d.ts +2 -2
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +120 -20
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/types.d.ts +68 -1
- package/dist/senpi/types.d.ts.map +1 -1
- package/dist/strategy/strategy-state.d.ts +1 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/tracking/spans/plugin-span-tracker.d.ts.map +1 -1
- package/dist/tracking/spans/plugin-span-tracker.js +4 -24
- package/dist/tracking/spans/plugin-span-tracker.js.map +1 -1
- package/dist/tracking/spans/redact-pii.d.ts +10 -0
- package/dist/tracking/spans/redact-pii.d.ts.map +1 -1
- package/dist/tracking/spans/redact-pii.js +80 -2
- package/dist/tracking/spans/redact-pii.js.map +1 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +1 -0
- package/dist/types/hooks.js.map +1 -1
- package/dist/types/runtime.d.ts +3 -0
- package/dist/types/runtime.d.ts.map +1 -1
- package/dist/types/strategy.d.ts +2 -2
- package/dist/types/strategy.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -8,11 +8,57 @@ import { resolveMargin } from './resolve-margin.js';
|
|
|
8
8
|
import { createLogger } from '../../utils/logger.js';
|
|
9
9
|
import { resolveOpenOrderType, resolveFeeOptimizedLimitOptions } from '../../utils/resolve-order-type.js';
|
|
10
10
|
import { emitTrackingDecision, emitTrackingActionResult } from '../tracking-helpers.js';
|
|
11
|
+
import { buildSignalAttrs, decisionAttrs, decisionErrorAttrs, emitBatchRejected, setPositionId, withSignalSpanScope, } from '../otel-helpers.js';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
11
13
|
const logger = createLogger("[actions][open-position]");
|
|
12
14
|
/** Hyperliquid minimum notional value (margin × leverage) required to open a position. */
|
|
13
15
|
const MIN_NOTIONAL_VALUE = 10;
|
|
14
16
|
/** Minimum leverage floor — positions cannot go below 1x. */
|
|
15
17
|
const MIN_LEVERAGE = 1;
|
|
18
|
+
// ─── Pure attr-shape helpers ─────────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Flatten a senpi `createPosition` response into the queryable subset of
|
|
21
|
+
* `action.open` span attrs. Extracted (per D5) so the senpi-response shape
|
|
22
|
+
* evolves in one place rather than scattered across the action body.
|
|
23
|
+
*/
|
|
24
|
+
function flattenOpenResultAttrs(result) {
|
|
25
|
+
const submitted = result.orders[0];
|
|
26
|
+
return {
|
|
27
|
+
"action.success": result.success,
|
|
28
|
+
"action.outcome": result.success ? "submitted" : "failed",
|
|
29
|
+
"action.order_id": submitted?.orderId,
|
|
30
|
+
"action.entry_price": submitted?.entryPrice,
|
|
31
|
+
"action.size": submitted?.size,
|
|
32
|
+
"action.filled": submitted?.filled,
|
|
33
|
+
"action.failure_reason": submitted?.error,
|
|
34
|
+
"action.fill_type": submitted?.executionAsMaker === true
|
|
35
|
+
? "maker"
|
|
36
|
+
: submitted?.executionAsMaker === false
|
|
37
|
+
? "taker"
|
|
38
|
+
: "unfilled",
|
|
39
|
+
"action.metadata.trace_id": result.metadata?.traceId,
|
|
40
|
+
"action.metadata.execution_id": result.metadata?.executionId,
|
|
41
|
+
...(submitted?.mainOrder ? {
|
|
42
|
+
"action.main_order.status": submitted.mainOrder.status,
|
|
43
|
+
"action.main_order.avg_price": submitted.mainOrder.avgPrice,
|
|
44
|
+
"action.main_order.requested_size": submitted.mainOrder.requestedSize,
|
|
45
|
+
} : {}),
|
|
46
|
+
...(submitted?.stopLoss ? {
|
|
47
|
+
"action.stop_loss.enabled": submitted.stopLoss.enabled,
|
|
48
|
+
"action.stop_loss.order_id": submitted.stopLoss.orderId,
|
|
49
|
+
"action.stop_loss.status": submitted.stopLoss.status,
|
|
50
|
+
"action.stop_loss.trigger_price": submitted.stopLoss.triggerPrice,
|
|
51
|
+
"action.stop_loss.error": submitted.stopLoss.error,
|
|
52
|
+
} : {}),
|
|
53
|
+
...(submitted?.takeProfit ? {
|
|
54
|
+
"action.take_profit.enabled": submitted.takeProfit.enabled,
|
|
55
|
+
"action.take_profit.order_id": submitted.takeProfit.orderId,
|
|
56
|
+
"action.take_profit.status": submitted.takeProfit.status,
|
|
57
|
+
"action.take_profit.trigger_price": submitted.takeProfit.triggerPrice,
|
|
58
|
+
"action.take_profit.error": submitted.takeProfit.error,
|
|
59
|
+
} : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
16
62
|
// ─── Open Position Action ───────────────────────────────────────────────────
|
|
17
63
|
export class OpenPositionAction {
|
|
18
64
|
config;
|
|
@@ -111,15 +157,19 @@ export class OpenPositionAction {
|
|
|
111
157
|
...(this.config.params ?? {}),
|
|
112
158
|
})
|
|
113
159
|
: undefined;
|
|
160
|
+
const fullContext = { ...contextData, scanResult };
|
|
161
|
+
const contextKeys = Object.keys(fullContext);
|
|
162
|
+
const minConfidence = this.config.min_confidence ?? 1;
|
|
114
163
|
// Get decision
|
|
115
164
|
let decision;
|
|
116
165
|
try {
|
|
117
166
|
decision = await this.decisionEngine.decide(this.config.decision_mode, {
|
|
118
167
|
prompt: interpolatedPrompt,
|
|
119
|
-
context:
|
|
168
|
+
context: fullContext,
|
|
120
169
|
model: this.config.decision_model,
|
|
121
170
|
actionType: this.actionType,
|
|
122
171
|
event,
|
|
172
|
+
minConfidence,
|
|
123
173
|
});
|
|
124
174
|
}
|
|
125
175
|
catch (error) {
|
|
@@ -151,13 +201,29 @@ export class OpenPositionAction {
|
|
|
151
201
|
},
|
|
152
202
|
};
|
|
153
203
|
emitTrackingActionResult(trackingCtx, event, address, errorResult);
|
|
204
|
+
const errAttrs = decisionErrorAttrs({
|
|
205
|
+
mode: this.config.decision_mode,
|
|
206
|
+
message,
|
|
207
|
+
minConfidence,
|
|
208
|
+
actionType: this.actionType,
|
|
209
|
+
model: this.config.decision_model,
|
|
210
|
+
contextKeys,
|
|
211
|
+
});
|
|
212
|
+
emitBatchRejected(this.context.otel, scanResult, "decision_error", "OPEN", errAttrs, error);
|
|
154
213
|
return errorResult;
|
|
155
214
|
}
|
|
156
215
|
logger.info('decision', decision);
|
|
157
216
|
const trackingCtx = this.trackingCtx();
|
|
158
|
-
const minConfidence = this.config.min_confidence ?? 1;
|
|
159
217
|
const isBelowConfidence = decision.execute && decision.confidence < minConfidence;
|
|
160
218
|
emitTrackingDecision(trackingCtx, event, address, scanResult, decision, minConfidence, interpolatedPrompt, contextData, isBelowConfidence ? 'below_confidence' : undefined);
|
|
219
|
+
const decAttrs = decisionAttrs({
|
|
220
|
+
mode: this.config.decision_mode,
|
|
221
|
+
decision,
|
|
222
|
+
minConfidence,
|
|
223
|
+
actionType: this.actionType,
|
|
224
|
+
model: this.config.decision_model,
|
|
225
|
+
contextKeys,
|
|
226
|
+
});
|
|
161
227
|
// Check decision
|
|
162
228
|
if (!decision.execute) {
|
|
163
229
|
logger.info('decision no execute', decision);
|
|
@@ -175,6 +241,7 @@ export class OpenPositionAction {
|
|
|
175
241
|
},
|
|
176
242
|
};
|
|
177
243
|
emitTrackingActionResult(trackingCtx, event, address, rejectedResult);
|
|
244
|
+
emitBatchRejected(this.context.otel, scanResult, "rejected_by_decision", "OPEN", decAttrs);
|
|
178
245
|
return rejectedResult;
|
|
179
246
|
}
|
|
180
247
|
// Check confidence threshold
|
|
@@ -194,12 +261,17 @@ export class OpenPositionAction {
|
|
|
194
261
|
},
|
|
195
262
|
};
|
|
196
263
|
emitTrackingActionResult(trackingCtx, event, address, belowConfResult);
|
|
264
|
+
emitBatchRejected(this.context.otel, scanResult, "rejected_by_confidence", "OPEN", {
|
|
265
|
+
...decAttrs,
|
|
266
|
+
"decision.skipped_reason": "below_confidence",
|
|
267
|
+
});
|
|
197
268
|
return belowConfResult;
|
|
198
269
|
}
|
|
199
270
|
// ── Unified signals path ──
|
|
200
271
|
const signals = this.normalizeSignals(decision.payload);
|
|
201
272
|
const meta = decisionMetaFromDecision(this.config, decision);
|
|
202
273
|
if (!signals.length) {
|
|
274
|
+
emitBatchRejected(this.context.otel, scanResult, "invalid_payload", "OPEN", decAttrs);
|
|
203
275
|
return {
|
|
204
276
|
success: false,
|
|
205
277
|
action_type: this.actionType,
|
|
@@ -208,7 +280,7 @@ export class OpenPositionAction {
|
|
|
208
280
|
data: { _decisionMeta: meta, _decisionPrompt: interpolatedPrompt ?? null },
|
|
209
281
|
};
|
|
210
282
|
}
|
|
211
|
-
return this.handleSignals(signals, event, strategyState, decision.reasoning, meta, interpolatedPrompt);
|
|
283
|
+
return this.handleSignals(signals, event, strategyState, decision.reasoning, meta, interpolatedPrompt, decAttrs);
|
|
212
284
|
}
|
|
213
285
|
normalizeSignals(payload) {
|
|
214
286
|
if (Array.isArray(payload.signals))
|
|
@@ -223,8 +295,10 @@ export class OpenPositionAction {
|
|
|
223
295
|
}
|
|
224
296
|
return [];
|
|
225
297
|
}
|
|
226
|
-
async handleSignals(signals, event, strategyState, reasoning, meta, decisionPrompt) {
|
|
298
|
+
async handleSignals(signals, event, strategyState, reasoning, meta, decisionPrompt, decAttrs) {
|
|
227
299
|
const address = event.address;
|
|
300
|
+
const eventScanResult = event.data?.scanResult;
|
|
301
|
+
const scannerId = eventScanResult?.scannerId;
|
|
228
302
|
const orderType = resolveOpenOrderType(this.config.params?.order_type);
|
|
229
303
|
const feeOptimizedLimitOptions = orderType === 'FEE_OPTIMIZED_LIMIT'
|
|
230
304
|
? resolveFeeOptimizedLimitOptions(this.config.params?.fee_optimized_limit_options)
|
|
@@ -251,45 +325,148 @@ export class OpenPositionAction {
|
|
|
251
325
|
return undefined;
|
|
252
326
|
})(),
|
|
253
327
|
(async () => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return undefined;
|
|
328
|
+
// Fetch withdrawable regardless of marginPct: it sizes margin_pct AND
|
|
329
|
+
// feeds the free-margin pre-flight (P1) below.
|
|
330
|
+
const chState = strategyState.getClearinghouseState()
|
|
331
|
+
?? await strategyState.fetchClearinghouseState();
|
|
332
|
+
return chState?.withdrawable;
|
|
260
333
|
})(),
|
|
261
334
|
]);
|
|
262
335
|
riskSnapshot = riskSnapResult;
|
|
263
336
|
withdrawable = withdrawableResult;
|
|
264
337
|
let remainingSlots = strategyState.getAvailableSlots?.() ?? 0;
|
|
338
|
+
// Free-margin pre-flight budget (P1). Undefined when withdrawable is unknown
|
|
339
|
+
// (no clearinghouse read) — in that case we do NOT block, preserving prior
|
|
340
|
+
// behavior. Decremented per submitted signal so later signals in the same
|
|
341
|
+
// batch are evaluated against the margin already consumed by earlier ones.
|
|
342
|
+
let remainingMargin = withdrawable;
|
|
265
343
|
const results = [];
|
|
266
344
|
let submittedCount = 0;
|
|
267
345
|
let failedCount = 0;
|
|
268
346
|
for (const signal of sorted) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
347
|
+
try {
|
|
348
|
+
const outcome = await this.processOpenSignal({
|
|
349
|
+
signal,
|
|
350
|
+
event,
|
|
351
|
+
scannerId,
|
|
352
|
+
strategyState,
|
|
353
|
+
orderType,
|
|
354
|
+
feeOptimizedLimitOptions,
|
|
355
|
+
reasoning,
|
|
356
|
+
decAttrs,
|
|
357
|
+
withdrawable,
|
|
358
|
+
remainingMargin,
|
|
359
|
+
remainingSlots,
|
|
360
|
+
});
|
|
361
|
+
results.push(outcome.result);
|
|
362
|
+
if (outcome.kind === 'submitted') {
|
|
363
|
+
submittedCount++;
|
|
364
|
+
remainingSlots--;
|
|
365
|
+
// Decrement the free-margin budget only on a real submit, so later
|
|
366
|
+
// signals are evaluated against margin already consumed (P1).
|
|
367
|
+
if (remainingMargin != null && typeof outcome.marginConsumed === 'number') {
|
|
368
|
+
remainingMargin -= outcome.marginConsumed;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else if (outcome.kind === 'failed') {
|
|
372
|
+
failedCount++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// withSignalSpanScope re-throws after stamping `exception` outcome /
|
|
377
|
+
// recording the exception event on the span. The per-signal failed
|
|
378
|
+
// result has already been pushed inside processOpenSignal; the
|
|
379
|
+
// failure must not interrupt the loop over remaining signals.
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const skippedCount = results.length - submittedCount - failedCount;
|
|
383
|
+
const actionResult = {
|
|
384
|
+
success: submittedCount > 0 || failedCount === 0,
|
|
385
|
+
action_type: this.actionType,
|
|
386
|
+
summary: `Open submit: ${submittedCount} submitted, ${failedCount} failed, ${skippedCount} skipped`,
|
|
387
|
+
data: {
|
|
388
|
+
results,
|
|
389
|
+
submittedCount,
|
|
390
|
+
failedCount,
|
|
391
|
+
skippedCount,
|
|
392
|
+
...(meta ? { _decisionMeta: meta } : {}),
|
|
393
|
+
...(riskSnapshot ? { _riskSnapshot: riskSnapshot } : {}),
|
|
394
|
+
_decisionPrompt: decisionPrompt ?? null,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
emitTrackingActionResult(this.trackingCtx(), event, address, actionResult);
|
|
398
|
+
return actionResult;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Process one signal end-to-end: gates → leverage/margin resolution →
|
|
402
|
+
* senpi.createPosition → hook fire. Opens a per-signal `signal.evaluated`
|
|
403
|
+
* funnel span (via `withSignalSpanScope`) and stamps the per-signal `position.id`
|
|
404
|
+
* on mint. Returns an outcome the caller applies to counters + results.
|
|
405
|
+
*
|
|
406
|
+
* The exception path inside the senpi.createPosition try/catch sets a
|
|
407
|
+
* 'failed' outcome and lets `withSignalSpanScope` complete normally — the
|
|
408
|
+
* outer for-loop's catch in `handleSignals` only fires on truly unexpected
|
|
409
|
+
* throws.
|
|
410
|
+
*/
|
|
411
|
+
async processOpenSignal(opts) {
|
|
412
|
+
const { signal, event, scannerId, strategyState, orderType, feeOptimizedLimitOptions, reasoning, decAttrs, withdrawable, remainingMargin, remainingSlots } = opts;
|
|
413
|
+
const address = event.address;
|
|
414
|
+
const asset = signal.asset;
|
|
415
|
+
const direction = signal.direction;
|
|
416
|
+
const signalScore = signal.score;
|
|
417
|
+
const signalDex = signal.dex;
|
|
418
|
+
const submittedDex = typeof signalDex === "string" && signalDex.trim() !== "" ? signalDex.trim() : "main";
|
|
419
|
+
const dexForLookup = submittedDex === "main" ? undefined : submittedDex;
|
|
420
|
+
const signalAttrs = {
|
|
421
|
+
...buildSignalAttrs(scannerId, {
|
|
422
|
+
asset,
|
|
423
|
+
direction: direction ?? null,
|
|
424
|
+
score: signalScore,
|
|
425
|
+
signalType: signal.signalType,
|
|
426
|
+
factors: signal.factors,
|
|
427
|
+
meta: signal.meta,
|
|
428
|
+
}, "OPEN"),
|
|
429
|
+
...(decAttrs ?? {}),
|
|
430
|
+
};
|
|
431
|
+
// The per-signal ALS frame (opened by withSignalSpanScope) carries
|
|
432
|
+
// `currentSpan` so descendant spans/logs inherit parent automatically;
|
|
433
|
+
// `positionId` is set on mint and never leaks to the next iteration
|
|
434
|
+
// (H1/H2/B06). The closure mutates `outcome` so we can return it after
|
|
435
|
+
// the scope completes.
|
|
436
|
+
let outcome = {
|
|
437
|
+
kind: 'failed',
|
|
438
|
+
result: { asset, direction, dex: submittedDex, score: signalScore, success: false, reason: 'unknown' },
|
|
439
|
+
};
|
|
440
|
+
await withSignalSpanScope(this.context.otel, signalAttrs, async (signalSpan) => {
|
|
441
|
+
const endSignal = (label, status) => {
|
|
442
|
+
signalSpan.setAttr("signal.outcome", label);
|
|
443
|
+
signalSpan.end(status ? { statusCode: status } : undefined);
|
|
444
|
+
};
|
|
275
445
|
if (remainingSlots <= 0) {
|
|
276
|
-
|
|
277
|
-
|
|
446
|
+
outcome = { kind: 'skipped', result: { asset, direction, dex: submittedDex, score: signalScore, success: true, reason: 'no_slots' } };
|
|
447
|
+
endSignal("no_slots");
|
|
448
|
+
return;
|
|
278
449
|
}
|
|
279
450
|
if (!direction || (direction !== 'LONG' && direction !== 'SHORT')) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
451
|
+
outcome = { kind: 'failed', result: { asset, direction, dex: submittedDex, score: signalScore, success: false, reason: 'invalid_direction' } };
|
|
452
|
+
endSignal("invalid_direction", "Error");
|
|
453
|
+
return;
|
|
283
454
|
}
|
|
284
455
|
if (strategyState.hasOpenPosition(asset, dexForLookup)) {
|
|
285
|
-
|
|
286
|
-
|
|
456
|
+
outcome = { kind: 'skipped', result: { asset, direction, dex: submittedDex, score: signalScore, success: true, reason: 'position_already_exists' } };
|
|
457
|
+
endSignal("position_already_exists");
|
|
458
|
+
return;
|
|
287
459
|
}
|
|
288
|
-
// Risk guard per-open (real-time via MCP, with per-asset cooldown)
|
|
460
|
+
// Risk guard per-open (real-time via MCP, with per-asset cooldown).
|
|
461
|
+
// C11: outcome + reason stamped on signal.evaluated directly; the
|
|
462
|
+
// separate risk_gate.evaluated log was redundant.
|
|
289
463
|
const gate = await this.context.riskGuard.checkGate(address, { candidateAsset: asset });
|
|
464
|
+
signalSpan.setAttr("gate.outcome", gate.gate);
|
|
465
|
+
signalSpan.setAttr("gate.reason", gate.reason);
|
|
290
466
|
if (gate.gate !== 'OPEN') {
|
|
291
|
-
|
|
292
|
-
|
|
467
|
+
outcome = { kind: 'skipped', result: { asset, direction, dex: submittedDex, score: signalScore, success: true, reason: `risk_gate_${gate.gate}` } };
|
|
468
|
+
endSignal(`rejected_by_gate_${gate.gate.toLowerCase()}`);
|
|
469
|
+
return;
|
|
293
470
|
}
|
|
294
471
|
// Leverage: LLM override > config-based resolution
|
|
295
472
|
const llmLeverage = typeof signal.leverage === 'number' ? signal.leverage : undefined;
|
|
@@ -325,111 +502,181 @@ export class OpenPositionAction {
|
|
|
325
502
|
slots: strategyState.config.slots,
|
|
326
503
|
withdrawable,
|
|
327
504
|
});
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
505
|
+
outcome = { kind: 'failed', result: { asset, direction, dex: submittedDex, score: signalScore, leverage, success: false, reason: 'no_margin_configured' } };
|
|
506
|
+
endSignal("no_margin_configured", "Error");
|
|
507
|
+
return;
|
|
331
508
|
}
|
|
332
509
|
const notionalValue = marginAmount * leverage;
|
|
333
510
|
if (notionalValue < MIN_NOTIONAL_VALUE) {
|
|
334
511
|
logger.warn(`Skipping ${asset}: notional value $${notionalValue.toFixed(2)} ` +
|
|
335
512
|
`(margin $${marginAmount.toFixed(2)} × ${leverage}x) is below minimum $${MIN_NOTIONAL_VALUE}`);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
513
|
+
outcome = { kind: 'failed', result: { asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, notionalValue, success: false, reason: 'below_min_notional' } };
|
|
514
|
+
endSignal("below_min_notional", "Error");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Free-margin pre-flight (P1): if we know free margin and it can't cover
|
|
518
|
+
// this position's margin, skip rather than fire a create the engine will
|
|
519
|
+
// reject (CREATE_INSUFFICIENT_FUNDS). The check is skipped when free
|
|
520
|
+
// margin is unknown (withdrawable null), preserving prior behavior.
|
|
521
|
+
if (remainingMargin != null && remainingMargin < marginAmount) {
|
|
522
|
+
logger.warn(`Skipping ${asset}: insufficient free margin ` +
|
|
523
|
+
`($${remainingMargin.toFixed(2)} available < $${marginAmount.toFixed(2)} required)`);
|
|
524
|
+
outcome = { kind: 'skipped', result: { asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, requiredMarginAmount: marginAmount, availableMarginAmount: remainingMargin, success: true, reason: 'insufficient_margin' } };
|
|
525
|
+
endSignal("insufficient_margin");
|
|
526
|
+
return;
|
|
339
527
|
}
|
|
340
528
|
const signalReason = signal.reason ?? signal.signalType ?? reasoning;
|
|
529
|
+
// Mint position.id at intent — before order submission, after confidence + risk gates.
|
|
530
|
+
// The OTEL ALS frame opened by withSignalSpanScope is per-signal, so the id is stamped
|
|
531
|
+
// on signal.evaluated, action.open, and downstream logs without leaking across signals.
|
|
532
|
+
const positionId = randomUUID();
|
|
533
|
+
setPositionId(positionId);
|
|
534
|
+
signalSpan.setAttr("position.id", positionId);
|
|
535
|
+
const actionSpan = this.context.otel.startSpan("action.open", {
|
|
536
|
+
parent: signalSpan,
|
|
537
|
+
attrs: {
|
|
538
|
+
"action.intent": "OPEN",
|
|
539
|
+
"action.asset": asset,
|
|
540
|
+
"action.direction": direction,
|
|
541
|
+
"action.order_type": orderType,
|
|
542
|
+
"action.dex": submittedDex,
|
|
543
|
+
"action.leverage": leverage,
|
|
544
|
+
"action.margin": marginAmount,
|
|
545
|
+
"action.risk_gate": gate.gate,
|
|
546
|
+
...(typeof withdrawable === "number" ? { "action.withdrawable_balance": withdrawable } : {}),
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
const createRequest = {
|
|
550
|
+
strategyWalletAddress: address,
|
|
551
|
+
orders: [
|
|
552
|
+
{
|
|
553
|
+
coin: asset,
|
|
554
|
+
direction,
|
|
555
|
+
leverage,
|
|
556
|
+
marginAmount,
|
|
557
|
+
orderType,
|
|
558
|
+
...(feeOptimizedLimitOptions ? { feeOptimizedLimitOptions } : {}),
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
reason: signalReason,
|
|
562
|
+
};
|
|
563
|
+
let submittedOrder;
|
|
564
|
+
let resultSuccess = false;
|
|
565
|
+
let signalEnded = false;
|
|
341
566
|
try {
|
|
342
567
|
logger.info('opening position', { wallet: address, asset, direction });
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
568
|
+
// D1: the standalone `action.order_execution` span is gone — ack
|
|
569
|
+
// latency + senpi metadata fold onto action.open directly.
|
|
570
|
+
const submitStart = Date.now();
|
|
571
|
+
let result;
|
|
572
|
+
try {
|
|
573
|
+
result = await this.context.senpi.createPosition(createRequest);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
actionSpan.setAttr("action.ack_latency_ms", Date.now() - submitStart);
|
|
577
|
+
actionSpan.recordException(err);
|
|
578
|
+
const e = err;
|
|
579
|
+
actionSpan.setAttr("exception.type", e?.name ?? "Error");
|
|
580
|
+
actionSpan.setAttr("action.outcome", "exception");
|
|
581
|
+
actionSpan.end({ statusCode: "Error" });
|
|
582
|
+
throw err;
|
|
583
|
+
}
|
|
584
|
+
actionSpan.setAttr("action.ack_latency_ms", Date.now() - submitStart);
|
|
585
|
+
// Source-of-truth blob for retrospection / fields not yet extracted.
|
|
586
|
+
// Span attrs carry the queryable subset; the log body carries the
|
|
587
|
+
// full response. Independent retention/sampling per the design.
|
|
588
|
+
this.context.otel.emitLog({
|
|
589
|
+
eventName: "senpi.order_response",
|
|
590
|
+
parent: actionSpan,
|
|
591
|
+
attrs: { "senpi.operation": "create_position" },
|
|
592
|
+
body: { request: createRequest, response: result },
|
|
356
593
|
});
|
|
594
|
+
actionSpan.setAttrs(flattenOpenResultAttrs(result));
|
|
595
|
+
actionSpan.end({ statusCode: result.success ? "Ok" : "Error" });
|
|
596
|
+
resultSuccess = result.success;
|
|
597
|
+
submittedOrder = result.orders[0];
|
|
357
598
|
if (!result.success) {
|
|
358
599
|
const orderErrors = result.orders
|
|
359
600
|
.filter((o) => o.error)
|
|
360
601
|
.map((o) => `${o.coin}: ${o.error}`)
|
|
361
602
|
.join('; ');
|
|
362
603
|
const detail = result.error || orderErrors || 'unknown error';
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
604
|
+
outcome = { kind: 'failed', result: {
|
|
605
|
+
asset, direction, dex: submittedDex, score: signalScore,
|
|
606
|
+
leverage, marginAmount, orderType,
|
|
607
|
+
success: false, reason: 'position_open_failed', error: detail,
|
|
608
|
+
} };
|
|
609
|
+
endSignal("position_open_failed", "Error");
|
|
610
|
+
signalEnded = true;
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
outcome = { kind: 'submitted', marginConsumed: marginAmount, result: {
|
|
614
|
+
asset, direction, dex: submittedDex, score: signalScore,
|
|
615
|
+
leverage, marginAmount, orderType,
|
|
616
|
+
orderId: submittedOrder?.orderId ?? null,
|
|
617
|
+
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
618
|
+
size: submittedOrder?.size ?? null,
|
|
619
|
+
filled: submittedOrder?.filled ?? null,
|
|
620
|
+
success: true, reason: 'submitted',
|
|
621
|
+
} };
|
|
622
|
+
endSignal("accepted", "Ok");
|
|
623
|
+
signalEnded = true;
|
|
370
624
|
}
|
|
371
|
-
const submittedOrder = result.orders[0];
|
|
372
|
-
await this.context.hookSystem.fire({
|
|
373
|
-
type: HOOK_EVENT.ON_OPEN_ORDER_SUBMITTED,
|
|
374
|
-
runtimeName: event.runtimeName,
|
|
375
|
-
address,
|
|
376
|
-
data: {
|
|
377
|
-
asset,
|
|
378
|
-
direction,
|
|
379
|
-
leverage,
|
|
380
|
-
marginAmount,
|
|
381
|
-
orderType,
|
|
382
|
-
wallet: address,
|
|
383
|
-
dex: submittedDex,
|
|
384
|
-
reasoning: signalReason,
|
|
385
|
-
orderId: submittedOrder?.orderId,
|
|
386
|
-
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
387
|
-
size: submittedOrder?.size ?? null,
|
|
388
|
-
filled: submittedOrder?.filled ?? null,
|
|
389
|
-
__correlationId: event.data.__correlationId,
|
|
390
|
-
},
|
|
391
|
-
timestamp: new Date().toISOString(),
|
|
392
|
-
});
|
|
393
|
-
results.push({
|
|
394
|
-
asset, direction, dex: submittedDex, score: signalScore,
|
|
395
|
-
leverage, marginAmount, orderType,
|
|
396
|
-
orderId: submittedOrder?.orderId ?? null,
|
|
397
|
-
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
398
|
-
size: submittedOrder?.size ?? null,
|
|
399
|
-
filled: submittedOrder?.filled ?? null,
|
|
400
|
-
success: true, reason: 'submitted',
|
|
401
|
-
});
|
|
402
|
-
submittedCount++;
|
|
403
|
-
remainingSlots--;
|
|
404
625
|
}
|
|
405
626
|
catch (error) {
|
|
406
627
|
const message = error instanceof Error ? error.message : String(error);
|
|
407
628
|
logger.error('open failed', { asset, err: message });
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
629
|
+
if (!signalEnded) {
|
|
630
|
+
signalSpan.recordException(error);
|
|
631
|
+
const e = error;
|
|
632
|
+
signalSpan.setAttr("exception.type", e?.name ?? "Error");
|
|
633
|
+
endSignal("exception", "Error");
|
|
634
|
+
signalEnded = true;
|
|
635
|
+
}
|
|
636
|
+
outcome = { kind: 'failed', result: {
|
|
637
|
+
asset, direction, dex: submittedDex, score: signalScore,
|
|
638
|
+
leverage, marginAmount, orderType,
|
|
639
|
+
success: false, reason: 'exception', error: message,
|
|
640
|
+
} };
|
|
641
|
+
return;
|
|
414
642
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
643
|
+
// B12 — hook fire happens AFTER the signal verdict is final and
|
|
644
|
+
// OUTSIDE the senpi.createPosition try/catch. A hook-fire failure
|
|
645
|
+
// must not flip the signal's outcome to "exception" when the order
|
|
646
|
+
// in fact succeeded. The hook failure is logged on the action span
|
|
647
|
+
// but never propagates.
|
|
648
|
+
if (resultSuccess) {
|
|
649
|
+
try {
|
|
650
|
+
await this.context.hookSystem.fire({
|
|
651
|
+
type: HOOK_EVENT.ON_OPEN_ORDER_SUBMITTED,
|
|
652
|
+
runtimeName: event.runtimeName,
|
|
653
|
+
address,
|
|
654
|
+
data: {
|
|
655
|
+
asset,
|
|
656
|
+
direction,
|
|
657
|
+
leverage,
|
|
658
|
+
marginAmount,
|
|
659
|
+
orderType,
|
|
660
|
+
wallet: address,
|
|
661
|
+
dex: submittedDex,
|
|
662
|
+
reasoning: signalReason,
|
|
663
|
+
orderId: submittedOrder?.orderId,
|
|
664
|
+
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
665
|
+
size: submittedOrder?.size ?? null,
|
|
666
|
+
filled: submittedOrder?.filled ?? null,
|
|
667
|
+
__correlationId: event.data.__correlationId,
|
|
668
|
+
},
|
|
669
|
+
timestamp: new Date().toISOString(),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
catch (hookErr) {
|
|
673
|
+
logger.warn('hook fire ON_OPEN_ORDER_SUBMITTED failed', {
|
|
674
|
+
asset, err: hookErr instanceof Error ? hookErr.message : String(hookErr),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
return outcome;
|
|
433
680
|
}
|
|
434
681
|
}
|
|
435
682
|
// ─── Factory & Manifest ─────────────────────────────────────────────────────
|