@senpi/runtime 1.1.0-dev.runtime-phase-2.20260609132603 → 1.1.0-dev.runtime-phase-2.20260610125851
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 +0 -16
- package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
- package/dist/actions/close-position/close-position.action.js +38 -299
- package/dist/actions/close-position/close-position.action.js.map +1 -1
- package/dist/actions/decision-engine.d.ts +0 -2
- 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 +0 -3
- package/dist/actions/llm-decision/factory.d.ts.map +1 -1
- package/dist/actions/llm-decision/factory.js +0 -1
- package/dist/actions/llm-decision/factory.js.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts +0 -13
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.js +68 -195
- package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts +0 -12
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +123 -346
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.d.ts +0 -6
- package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.js +0 -48
- package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
- package/dist/runtime/create-actions.d.ts.map +1 -1
- package/dist/runtime/create-actions.js +0 -2
- package/dist/runtime/create-actions.js.map +1 -1
- package/dist/runtime/create-runtime-context.d.ts +0 -2
- package/dist/runtime/create-runtime-context.d.ts.map +1 -1
- package/dist/runtime/create-runtime-context.js +0 -1
- 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 +1 -2
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +0 -24
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +0 -1
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +0 -5
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +4 -127
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/scanners/engine/data-providers.d.ts +0 -2
- 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 +9 -68
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts +0 -2
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +0 -1
- 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 +20 -120
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/types.d.ts +1 -68
- 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 +24 -4
- package/dist/tracking/spans/plugin-span-tracker.js.map +1 -1
- package/dist/tracking/spans/redact-pii.d.ts +0 -10
- package/dist/tracking/spans/redact-pii.d.ts.map +1 -1
- package/dist/tracking/spans/redact-pii.js +2 -80
- package/dist/tracking/spans/redact-pii.js.map +1 -1
- package/dist/types/runtime.d.ts +0 -3
- 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
- package/dist/actions/otel-helpers.d.ts +0 -65
- package/dist/actions/otel-helpers.d.ts.map +0 -1
- package/dist/actions/otel-helpers.js +0 -122
- package/dist/actions/otel-helpers.js.map +0 -1
- package/dist/otel/context.d.ts +0 -24
- package/dist/otel/context.d.ts.map +0 -1
- package/dist/otel/context.js +0 -29
- package/dist/otel/context.js.map +0 -1
- package/dist/otel/emitter.d.ts +0 -73
- package/dist/otel/emitter.d.ts.map +0 -1
- package/dist/otel/emitter.js +0 -248
- package/dist/otel/emitter.js.map +0 -1
- package/dist/otel/encode.d.ts +0 -48
- package/dist/otel/encode.d.ts.map +0 -1
- package/dist/otel/encode.js +0 -100
- package/dist/otel/encode.js.map +0 -1
- package/dist/otel/index.d.ts +0 -13
- package/dist/otel/index.d.ts.map +0 -1
- package/dist/otel/index.js +0 -10
- package/dist/otel/index.js.map +0 -1
- package/dist/otel/transport.d.ts +0 -57
- package/dist/otel/transport.d.ts.map +0 -1
- package/dist/otel/transport.js +0 -208
- package/dist/otel/transport.js.map +0 -1
- package/dist/otel/types.d.ts +0 -62
- package/dist/otel/types.d.ts.map +0 -1
- package/dist/otel/types.js +0 -2
- package/dist/otel/types.js.map +0 -1
|
@@ -8,57 +8,11 @@ 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';
|
|
13
11
|
const logger = createLogger("[actions][open-position]");
|
|
14
12
|
/** Hyperliquid minimum notional value (margin × leverage) required to open a position. */
|
|
15
13
|
const MIN_NOTIONAL_VALUE = 10;
|
|
16
14
|
/** Minimum leverage floor — positions cannot go below 1x. */
|
|
17
15
|
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
|
-
}
|
|
62
16
|
// ─── Open Position Action ───────────────────────────────────────────────────
|
|
63
17
|
export class OpenPositionAction {
|
|
64
18
|
config;
|
|
@@ -125,7 +79,16 @@ export class OpenPositionAction {
|
|
|
125
79
|
error: 'strategy_address_mismatch',
|
|
126
80
|
};
|
|
127
81
|
}
|
|
128
|
-
// Early exit: no slots
|
|
82
|
+
// Early exit: no slots.
|
|
83
|
+
// NOTE re "zero slots skips cap alerts": we intentionally do NOT evaluate risk
|
|
84
|
+
// halts/cap self-pause here. It would add a per-signal getPnlHistoryWithSnapshot
|
|
85
|
+
// MCP call on the full-book path (costly on a full + high-signal-rate strategy),
|
|
86
|
+
// and that halt data is fetched nowhere else to reuse (only risk-guard.checkGate
|
|
87
|
+
// fetches it; the DSL monitor fetches clearinghouse = different data). Self-pause
|
|
88
|
+
// alerts still fire via the normal open path once a slot frees and a signal
|
|
89
|
+
// arrives (delayed, not lost). The durable fix — a periodic lifecycle driven off
|
|
90
|
+
// the monitor tick / bus events (also catches a fully-silent producer) — is
|
|
91
|
+
// roadmap §5 (the merge), deliberately not blended into this §4 short-term PR.
|
|
129
92
|
if (!strategyState.getAvailableSlots?.()) {
|
|
130
93
|
return {
|
|
131
94
|
success: true,
|
|
@@ -157,19 +120,15 @@ export class OpenPositionAction {
|
|
|
157
120
|
...(this.config.params ?? {}),
|
|
158
121
|
})
|
|
159
122
|
: undefined;
|
|
160
|
-
const fullContext = { ...contextData, scanResult };
|
|
161
|
-
const contextKeys = Object.keys(fullContext);
|
|
162
|
-
const minConfidence = this.config.min_confidence ?? 1;
|
|
163
123
|
// Get decision
|
|
164
124
|
let decision;
|
|
165
125
|
try {
|
|
166
126
|
decision = await this.decisionEngine.decide(this.config.decision_mode, {
|
|
167
127
|
prompt: interpolatedPrompt,
|
|
168
|
-
context:
|
|
128
|
+
context: { ...contextData, scanResult },
|
|
169
129
|
model: this.config.decision_model,
|
|
170
130
|
actionType: this.actionType,
|
|
171
131
|
event,
|
|
172
|
-
minConfidence,
|
|
173
132
|
});
|
|
174
133
|
}
|
|
175
134
|
catch (error) {
|
|
@@ -201,29 +160,13 @@ export class OpenPositionAction {
|
|
|
201
160
|
},
|
|
202
161
|
};
|
|
203
162
|
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);
|
|
213
163
|
return errorResult;
|
|
214
164
|
}
|
|
215
165
|
logger.info('decision', decision);
|
|
216
166
|
const trackingCtx = this.trackingCtx();
|
|
167
|
+
const minConfidence = this.config.min_confidence ?? 1;
|
|
217
168
|
const isBelowConfidence = decision.execute && decision.confidence < minConfidence;
|
|
218
169
|
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
|
-
});
|
|
227
170
|
// Check decision
|
|
228
171
|
if (!decision.execute) {
|
|
229
172
|
logger.info('decision no execute', decision);
|
|
@@ -241,7 +184,6 @@ export class OpenPositionAction {
|
|
|
241
184
|
},
|
|
242
185
|
};
|
|
243
186
|
emitTrackingActionResult(trackingCtx, event, address, rejectedResult);
|
|
244
|
-
emitBatchRejected(this.context.otel, scanResult, "rejected_by_decision", "OPEN", decAttrs);
|
|
245
187
|
return rejectedResult;
|
|
246
188
|
}
|
|
247
189
|
// Check confidence threshold
|
|
@@ -261,17 +203,12 @@ export class OpenPositionAction {
|
|
|
261
203
|
},
|
|
262
204
|
};
|
|
263
205
|
emitTrackingActionResult(trackingCtx, event, address, belowConfResult);
|
|
264
|
-
emitBatchRejected(this.context.otel, scanResult, "rejected_by_confidence", "OPEN", {
|
|
265
|
-
...decAttrs,
|
|
266
|
-
"decision.skipped_reason": "below_confidence",
|
|
267
|
-
});
|
|
268
206
|
return belowConfResult;
|
|
269
207
|
}
|
|
270
208
|
// ── Unified signals path ──
|
|
271
209
|
const signals = this.normalizeSignals(decision.payload);
|
|
272
210
|
const meta = decisionMetaFromDecision(this.config, decision);
|
|
273
211
|
if (!signals.length) {
|
|
274
|
-
emitBatchRejected(this.context.otel, scanResult, "invalid_payload", "OPEN", decAttrs);
|
|
275
212
|
return {
|
|
276
213
|
success: false,
|
|
277
214
|
action_type: this.actionType,
|
|
@@ -280,7 +217,7 @@ export class OpenPositionAction {
|
|
|
280
217
|
data: { _decisionMeta: meta, _decisionPrompt: interpolatedPrompt ?? null },
|
|
281
218
|
};
|
|
282
219
|
}
|
|
283
|
-
return this.handleSignals(signals, event, strategyState, decision.reasoning, meta, interpolatedPrompt
|
|
220
|
+
return this.handleSignals(signals, event, strategyState, decision.reasoning, meta, interpolatedPrompt);
|
|
284
221
|
}
|
|
285
222
|
normalizeSignals(payload) {
|
|
286
223
|
if (Array.isArray(payload.signals))
|
|
@@ -295,10 +232,8 @@ export class OpenPositionAction {
|
|
|
295
232
|
}
|
|
296
233
|
return [];
|
|
297
234
|
}
|
|
298
|
-
async handleSignals(signals, event, strategyState, reasoning, meta, decisionPrompt
|
|
235
|
+
async handleSignals(signals, event, strategyState, reasoning, meta, decisionPrompt) {
|
|
299
236
|
const address = event.address;
|
|
300
|
-
const eventScanResult = event.data?.scanResult;
|
|
301
|
-
const scannerId = eventScanResult?.scannerId;
|
|
302
237
|
const orderType = resolveOpenOrderType(this.config.params?.order_type);
|
|
303
238
|
const feeOptimizedLimitOptions = orderType === 'FEE_OPTIMIZED_LIMIT'
|
|
304
239
|
? resolveFeeOptimizedLimitOptions(this.config.params?.fee_optimized_limit_options)
|
|
@@ -336,137 +271,38 @@ export class OpenPositionAction {
|
|
|
336
271
|
withdrawable = withdrawableResult;
|
|
337
272
|
let remainingSlots = strategyState.getAvailableSlots?.() ?? 0;
|
|
338
273
|
// 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
|
-
//
|
|
341
|
-
//
|
|
274
|
+
// (no clearinghouse read) — in that case we do NOT block, preserving prior behavior.
|
|
275
|
+
// Decremented per submitted signal so later signals in the same batch are
|
|
276
|
+
// evaluated against the margin already consumed by earlier ones.
|
|
342
277
|
let remainingMargin = withdrawable;
|
|
343
278
|
const results = [];
|
|
344
279
|
let submittedCount = 0;
|
|
345
280
|
let failedCount = 0;
|
|
346
281
|
for (const signal of sorted) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
};
|
|
282
|
+
const asset = signal.asset;
|
|
283
|
+
const direction = signal.direction;
|
|
284
|
+
const signalScore = signal.score;
|
|
285
|
+
const signalDex = signal.dex;
|
|
286
|
+
const submittedDex = typeof signalDex === "string" && signalDex.trim() !== "" ? signalDex.trim() : "main";
|
|
287
|
+
const dexForLookup = submittedDex === "main" ? undefined : submittedDex;
|
|
445
288
|
if (remainingSlots <= 0) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return;
|
|
289
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: 'no_slots' });
|
|
290
|
+
continue;
|
|
449
291
|
}
|
|
450
292
|
if (!direction || (direction !== 'LONG' && direction !== 'SHORT')) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
293
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, success: false, reason: 'invalid_direction' });
|
|
294
|
+
failedCount++;
|
|
295
|
+
continue;
|
|
454
296
|
}
|
|
455
297
|
if (strategyState.hasOpenPosition(asset, dexForLookup)) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return;
|
|
298
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: 'position_already_exists' });
|
|
299
|
+
continue;
|
|
459
300
|
}
|
|
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.
|
|
301
|
+
// Risk guard per-open (real-time via MCP, with per-asset cooldown)
|
|
463
302
|
const gate = await this.context.riskGuard.checkGate(address, { candidateAsset: asset });
|
|
464
|
-
signalSpan.setAttr("gate.outcome", gate.gate);
|
|
465
|
-
signalSpan.setAttr("gate.reason", gate.reason);
|
|
466
303
|
if (gate.gate !== 'OPEN') {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return;
|
|
304
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: `risk_gate_${gate.gate}` });
|
|
305
|
+
continue;
|
|
470
306
|
}
|
|
471
307
|
// Leverage: LLM override > config-based resolution
|
|
472
308
|
const llmLeverage = typeof signal.leverage === 'number' ? signal.leverage : undefined;
|
|
@@ -502,181 +338,122 @@ export class OpenPositionAction {
|
|
|
502
338
|
slots: strategyState.config.slots,
|
|
503
339
|
withdrawable,
|
|
504
340
|
});
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
341
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, success: false, reason: 'no_margin_configured' });
|
|
342
|
+
failedCount++;
|
|
343
|
+
continue;
|
|
508
344
|
}
|
|
509
345
|
const notionalValue = marginAmount * leverage;
|
|
510
346
|
if (notionalValue < MIN_NOTIONAL_VALUE) {
|
|
511
347
|
logger.warn(`Skipping ${asset}: notional value $${notionalValue.toFixed(2)} ` +
|
|
512
348
|
`(margin $${marginAmount.toFixed(2)} × ${leverage}x) is below minimum $${MIN_NOTIONAL_VALUE}`);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
349
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, notionalValue, success: false, reason: 'below_min_notional' });
|
|
350
|
+
failedCount++;
|
|
351
|
+
continue;
|
|
516
352
|
}
|
|
517
|
-
// Free-margin pre-flight (P1): if we know free margin and it can't cover
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
// margin is unknown (withdrawable null), preserving prior behavior.
|
|
353
|
+
// Free-margin pre-flight (P1): if we know free margin and it can't cover this
|
|
354
|
+
// position's margin, skip rather than fire a create the engine will reject
|
|
355
|
+
// (CREATE_INSUFFICIENT_FUNDS). Skip the check when free margin is unknown.
|
|
521
356
|
if (remainingMargin != null && remainingMargin < marginAmount) {
|
|
522
357
|
logger.warn(`Skipping ${asset}: insufficient free margin ` +
|
|
523
358
|
`($${remainingMargin.toFixed(2)} available < $${marginAmount.toFixed(2)} required)`);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return;
|
|
359
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, requiredMarginAmount: marginAmount, availableMarginAmount: remainingMargin, success: true, reason: 'insufficient_margin' });
|
|
360
|
+
continue;
|
|
527
361
|
}
|
|
528
362
|
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;
|
|
566
363
|
try {
|
|
567
364
|
logger.info('opening position', { wallet: address, asset, direction });
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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 },
|
|
365
|
+
const result = await this.context.senpi.createPosition({
|
|
366
|
+
strategyWalletAddress: address,
|
|
367
|
+
orders: [
|
|
368
|
+
{
|
|
369
|
+
coin: asset,
|
|
370
|
+
direction,
|
|
371
|
+
leverage,
|
|
372
|
+
marginAmount,
|
|
373
|
+
orderType,
|
|
374
|
+
...(feeOptimizedLimitOptions ? { feeOptimizedLimitOptions } : {}),
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
reason: signalReason,
|
|
593
378
|
});
|
|
594
|
-
actionSpan.setAttrs(flattenOpenResultAttrs(result));
|
|
595
|
-
actionSpan.end({ statusCode: result.success ? "Ok" : "Error" });
|
|
596
|
-
resultSuccess = result.success;
|
|
597
|
-
submittedOrder = result.orders[0];
|
|
598
379
|
if (!result.success) {
|
|
599
380
|
const orderErrors = result.orders
|
|
600
381
|
.filter((o) => o.error)
|
|
601
382
|
.map((o) => `${o.coin}: ${o.error}`)
|
|
602
383
|
.join('; ');
|
|
603
384
|
const detail = result.error || orderErrors || 'unknown error';
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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;
|
|
385
|
+
results.push({
|
|
386
|
+
asset, direction, dex: submittedDex, score: signalScore,
|
|
387
|
+
leverage, marginAmount, orderType,
|
|
388
|
+
success: false, reason: 'position_open_failed', error: detail,
|
|
389
|
+
});
|
|
390
|
+
failedCount++;
|
|
391
|
+
continue;
|
|
624
392
|
}
|
|
393
|
+
const submittedOrder = result.orders[0];
|
|
394
|
+
await this.context.hookSystem.fire({
|
|
395
|
+
type: HOOK_EVENT.ON_OPEN_ORDER_SUBMITTED,
|
|
396
|
+
runtimeName: event.runtimeName,
|
|
397
|
+
address,
|
|
398
|
+
data: {
|
|
399
|
+
asset,
|
|
400
|
+
direction,
|
|
401
|
+
leverage,
|
|
402
|
+
marginAmount,
|
|
403
|
+
orderType,
|
|
404
|
+
wallet: address,
|
|
405
|
+
dex: submittedDex,
|
|
406
|
+
reasoning: signalReason,
|
|
407
|
+
orderId: submittedOrder?.orderId,
|
|
408
|
+
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
409
|
+
size: submittedOrder?.size ?? null,
|
|
410
|
+
filled: submittedOrder?.filled ?? null,
|
|
411
|
+
__correlationId: event.data.__correlationId,
|
|
412
|
+
},
|
|
413
|
+
timestamp: new Date().toISOString(),
|
|
414
|
+
});
|
|
415
|
+
results.push({
|
|
416
|
+
asset, direction, dex: submittedDex, score: signalScore,
|
|
417
|
+
leverage, marginAmount, orderType,
|
|
418
|
+
orderId: submittedOrder?.orderId ?? null,
|
|
419
|
+
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
420
|
+
size: submittedOrder?.size ?? null,
|
|
421
|
+
filled: submittedOrder?.filled ?? null,
|
|
422
|
+
success: true, reason: 'submitted',
|
|
423
|
+
});
|
|
424
|
+
submittedCount++;
|
|
425
|
+
remainingSlots--;
|
|
426
|
+
if (remainingMargin != null)
|
|
427
|
+
remainingMargin -= marginAmount;
|
|
625
428
|
}
|
|
626
429
|
catch (error) {
|
|
627
430
|
const message = error instanceof Error ? error.message : String(error);
|
|
628
431
|
logger.error('open failed', { asset, err: message });
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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;
|
|
642
|
-
}
|
|
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
|
-
}
|
|
432
|
+
results.push({
|
|
433
|
+
asset, direction, dex: submittedDex, score: signalScore,
|
|
434
|
+
leverage, marginAmount, orderType,
|
|
435
|
+
success: false, reason: 'exception', error: message,
|
|
436
|
+
});
|
|
437
|
+
failedCount++;
|
|
677
438
|
}
|
|
678
|
-
}
|
|
679
|
-
|
|
439
|
+
}
|
|
440
|
+
const skippedCount = results.length - submittedCount - failedCount;
|
|
441
|
+
const actionResult = {
|
|
442
|
+
success: submittedCount > 0 || failedCount === 0,
|
|
443
|
+
action_type: this.actionType,
|
|
444
|
+
summary: `Open submit: ${submittedCount} submitted, ${failedCount} failed, ${skippedCount} skipped`,
|
|
445
|
+
data: {
|
|
446
|
+
results,
|
|
447
|
+
submittedCount,
|
|
448
|
+
failedCount,
|
|
449
|
+
skippedCount,
|
|
450
|
+
...(meta ? { _decisionMeta: meta } : {}),
|
|
451
|
+
...(riskSnapshot ? { _riskSnapshot: riskSnapshot } : {}),
|
|
452
|
+
_decisionPrompt: decisionPrompt ?? null,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
emitTrackingActionResult(this.trackingCtx(), event, address, actionResult);
|
|
456
|
+
return actionResult;
|
|
680
457
|
}
|
|
681
458
|
}
|
|
682
459
|
// ─── Factory & Manifest ─────────────────────────────────────────────────────
|