@senpi/runtime 3.0.6-dev.runtime-phase-2.20260630134145 → 3.0.6-dev.runtime-phase-2.20260630134327
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 +1 -1
- package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
- package/dist/actions/close-position/close-position.action.js +109 -78
- package/dist/actions/close-position/close-position.action.js.map +1 -1
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +40 -28
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/event-derivation.d.ts +60 -0
- package/dist/actions/event-derivation.d.ts.map +1 -0
- package/dist/actions/event-derivation.js +179 -0
- package/dist/actions/event-derivation.js.map +1 -0
- package/dist/actions/llm-decision/anthropic-llm-decision.d.ts.map +1 -1
- package/dist/actions/llm-decision/anthropic-llm-decision.js +1 -2
- package/dist/actions/llm-decision/anthropic-llm-decision.js.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.js +1 -2
- package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts +1 -1
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +283 -217
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.js +2 -4
- package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
- package/dist/actions/tracking-helpers.d.ts +41 -3
- package/dist/actions/tracking-helpers.d.ts.map +1 -1
- package/dist/actions/tracking-helpers.js +100 -11
- package/dist/actions/tracking-helpers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +3 -0
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +76 -28
- package/dist/dsl/monitor/create-monitor.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts +2 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +84 -38
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +10 -0
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +42 -4
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/dsl/plugin/options.d.ts +3 -0
- package/dist/dsl/plugin/options.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/recording/otel/attributes.d.ts +2 -0
- package/dist/recording/otel/attributes.d.ts.map +1 -1
- package/dist/recording/otel/attributes.js +1 -1
- package/dist/recording/otel/attributes.js.map +1 -1
- package/dist/recording/otel/mappers.d.ts.map +1 -1
- package/dist/recording/otel/mappers.js +1 -0
- package/dist/recording/otel/mappers.js.map +1 -1
- package/dist/recording/records.d.ts +7 -1
- package/dist/recording/records.d.ts.map +1 -1
- package/dist/recording/wiring/record-builders.d.ts.map +1 -1
- package/dist/recording/wiring/record-builders.js +5 -2
- package/dist/recording/wiring/record-builders.js.map +1 -1
- package/dist/runtime/auto-update/coordinator.js +2 -2
- package/dist/runtime/auto-update/coordinator.js.map +1 -1
- package/dist/runtime/create-actions.d.ts.map +1 -1
- package/dist/runtime/create-actions.js +40 -15
- package/dist/runtime/create-actions.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +2 -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/domain-events.d.ts +43 -0
- package/dist/runtime/domain-events.d.ts.map +1 -0
- package/dist/runtime/domain-events.js +181 -0
- package/dist/runtime/domain-events.js.map +1 -0
- package/dist/runtime/lifecycle-events.d.ts +43 -0
- package/dist/runtime/lifecycle-events.d.ts.map +1 -0
- package/dist/runtime/lifecycle-events.js +139 -0
- package/dist/runtime/lifecycle-events.js.map +1 -0
- package/dist/runtime/notifications.js +2 -2
- package/dist/runtime/notifications.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +120 -120
- package/dist/runtime/runtime.d.ts +11 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +159 -88
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/strategy-lifecycle.d.ts.map +1 -1
- package/dist/runtime/strategy-lifecycle.js +13 -1
- package/dist/runtime/strategy-lifecycle.js.map +1 -1
- package/dist/runtime-api/routes/signals.d.ts +1 -1
- package/dist/runtime-api/routes/signals.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.js +2 -8
- package/dist/runtime-api/routes/signals.js.map +1 -1
- package/dist/scanners/engine/data-providers.d.ts +3 -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 +18 -0
- package/dist/scanners/engine/engine.d.ts.map +1 -1
- package/dist/scanners/engine/engine.js +93 -2
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/engine/input-resolver.d.ts.map +1 -1
- package/dist/scanners/engine/input-resolver.js +9 -7
- package/dist/scanners/engine/input-resolver.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts +3 -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.map +1 -1
- package/dist/senpi/client.js +7 -5
- package/dist/senpi/client.js.map +1 -1
- package/dist/skills-manager/coordinator.js +3 -3
- package/dist/skills-manager/coordinator.js.map +1 -1
- package/dist/telemetry/otel-sdk.d.ts +8 -2
- package/dist/telemetry/otel-sdk.d.ts.map +1 -1
- package/dist/telemetry/otel-sdk.js +17 -4
- package/dist/telemetry/otel-sdk.js.map +1 -1
- package/dist/telemetry/trace-identity.d.ts +32 -0
- package/dist/telemetry/trace-identity.d.ts.map +1 -0
- package/dist/telemetry/trace-identity.js +59 -0
- package/dist/telemetry/trace-identity.js.map +1 -0
- package/dist/telemetry/tracing.d.ts +32 -0
- package/dist/telemetry/tracing.d.ts.map +1 -0
- package/dist/telemetry/tracing.js +70 -0
- package/dist/telemetry/tracing.js.map +1 -0
- package/dist/tracking/context.d.ts +6 -0
- package/dist/tracking/context.d.ts.map +1 -1
- package/dist/tracking/context.js.map +1 -1
- package/dist/tracking/spans/plugin-span-tracker.d.ts.map +1 -1
- package/dist/tracking/spans/plugin-span-tracker.js +12 -12
- package/dist/tracking/spans/plugin-span-tracker.js.map +1 -1
- package/dist/tracking/spans/redact-pii.d.ts +3 -3
- package/dist/tracking/spans/redact-pii.d.ts.map +1 -1
- package/dist/tracking/spans/redact-pii.js +6 -7
- package/dist/tracking/spans/redact-pii.js.map +1 -1
- package/dist/types/scanner.d.ts +2 -0
- package/dist/types/scanner.d.ts.map +1 -1
- package/dist/utils/event-catalog.d.ts +269 -0
- package/dist/utils/event-catalog.d.ts.map +1 -0
- package/dist/utils/event-catalog.js +91 -0
- package/dist/utils/event-catalog.js.map +1 -0
- package/dist/utils/logger.d.ts +58 -5
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +359 -27
- package/dist/utils/logger.js.map +1 -1
- package/package.json +5 -1
|
@@ -8,7 +8,9 @@ import { resolveMarginWithSource } from './resolve-margin.js';
|
|
|
8
8
|
import { createLogger } from '../../utils/logger.js';
|
|
9
9
|
import { positionRegistryKey } from '../../tracking/position-id-registry.js';
|
|
10
10
|
import { resolveOpenOrderType, resolveFeeOptimizedLimitOptions } from '../../utils/resolve-order-type.js';
|
|
11
|
-
import { emitTrackingDecision, emitTrackingActionResult, emitOrderPlaced, emitOrderFilled, emitOrderFailed } from '../tracking-helpers.js';
|
|
11
|
+
import { emitTrackingDecision, emitTrackingActionResult, emitOrderPlaced, emitOrderFilled, emitOrderFailed, buildActionEventContext } from '../tracking-helpers.js';
|
|
12
|
+
import { withSpan } from '../../telemetry/tracing.js';
|
|
13
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
12
14
|
const logger = createLogger("[actions][open-position]");
|
|
13
15
|
/** Hyperliquid minimum notional value (margin × leverage) required to open a position. */
|
|
14
16
|
const MIN_NOTIONAL_VALUE = 10;
|
|
@@ -30,15 +32,16 @@ export class OpenPositionAction {
|
|
|
30
32
|
this.decisionEngine.register(new RuleDecisionStrategy());
|
|
31
33
|
this.decisionEngine.register(new NoDecisionStrategy());
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
+
eventCtx() {
|
|
36
|
+
return buildActionEventContext({
|
|
35
37
|
hookSystem: this.context.hookSystem,
|
|
36
38
|
actionName: this.name,
|
|
37
39
|
actionType: this.actionType,
|
|
38
40
|
decisionMode: this.config.decision_mode,
|
|
39
41
|
decisionModel: this.config.decision_model,
|
|
40
42
|
minConfidence: this.config.min_confidence,
|
|
41
|
-
|
|
43
|
+
positionIdRegistry: this.context.positionIdRegistry,
|
|
44
|
+
});
|
|
42
45
|
}
|
|
43
46
|
async handle(event) {
|
|
44
47
|
// Telemetry: pass start = "action began handling the scan event" (NOT "LLM call dispatched").
|
|
@@ -145,8 +148,8 @@ export class OpenPositionAction {
|
|
|
145
148
|
model: this.config.decision_model,
|
|
146
149
|
});
|
|
147
150
|
await this.context.notifications.notify(notificationMessage).catch(() => { });
|
|
148
|
-
const
|
|
149
|
-
emitTrackingDecision(
|
|
151
|
+
const eventCtx = this.eventCtx();
|
|
152
|
+
emitTrackingDecision(eventCtx, event, address, scanResult, { error: message }, undefined, interpolatedPrompt, contextData);
|
|
150
153
|
const errorResult = {
|
|
151
154
|
success: false,
|
|
152
155
|
action_type: this.actionType,
|
|
@@ -165,15 +168,15 @@ export class OpenPositionAction {
|
|
|
165
168
|
_decisionPrompt: interpolatedPrompt ?? null,
|
|
166
169
|
},
|
|
167
170
|
};
|
|
168
|
-
emitTrackingActionResult(
|
|
171
|
+
emitTrackingActionResult(eventCtx, event, address, errorResult, { scanResult, decisionStartedAt, decisionEndedAt });
|
|
169
172
|
return errorResult;
|
|
170
173
|
}
|
|
171
174
|
logger.info('decision', decision);
|
|
172
|
-
const
|
|
175
|
+
const eventCtx = this.eventCtx();
|
|
173
176
|
const minConfidence = this.config.min_confidence ?? 1;
|
|
174
177
|
const trackingExtras = { scanResult, decisionStartedAt, decisionEndedAt, minConfidence };
|
|
175
178
|
const isBelowConfidence = decision.execute && decision.confidence < minConfidence;
|
|
176
|
-
emitTrackingDecision(
|
|
179
|
+
emitTrackingDecision(eventCtx, event, address, scanResult, decision, minConfidence, interpolatedPrompt, contextData, isBelowConfidence ? 'below_confidence' : undefined);
|
|
177
180
|
// Check decision
|
|
178
181
|
if (!decision.execute) {
|
|
179
182
|
logger.info('decision no execute', decision);
|
|
@@ -190,7 +193,7 @@ export class OpenPositionAction {
|
|
|
190
193
|
_decisionPrompt: interpolatedPrompt ?? null,
|
|
191
194
|
},
|
|
192
195
|
};
|
|
193
|
-
emitTrackingActionResult(
|
|
196
|
+
emitTrackingActionResult(eventCtx, event, address, rejectedResult, trackingExtras);
|
|
194
197
|
return rejectedResult;
|
|
195
198
|
}
|
|
196
199
|
// Check confidence threshold
|
|
@@ -209,7 +212,7 @@ export class OpenPositionAction {
|
|
|
209
212
|
_decisionPrompt: interpolatedPrompt ?? null,
|
|
210
213
|
},
|
|
211
214
|
};
|
|
212
|
-
emitTrackingActionResult(
|
|
215
|
+
emitTrackingActionResult(eventCtx, event, address, belowConfResult, trackingExtras);
|
|
213
216
|
return belowConfResult;
|
|
214
217
|
}
|
|
215
218
|
// ── Unified signals path ──
|
|
@@ -223,7 +226,7 @@ export class OpenPositionAction {
|
|
|
223
226
|
error: 'invalid_payload',
|
|
224
227
|
data: { _decisionMeta: meta, _decisionPrompt: interpolatedPrompt ?? null },
|
|
225
228
|
};
|
|
226
|
-
emitTrackingActionResult(
|
|
229
|
+
emitTrackingActionResult(eventCtx, event, address, invalidPayloadResult, trackingExtras);
|
|
227
230
|
return invalidPayloadResult;
|
|
228
231
|
}
|
|
229
232
|
return this.handleSignals(signals, event, strategyState, decision.reasoning, meta, interpolatedPrompt, trackingExtras);
|
|
@@ -310,7 +313,7 @@ export class OpenPositionAction {
|
|
|
310
313
|
_decisionPrompt: decisionPrompt ?? null,
|
|
311
314
|
},
|
|
312
315
|
};
|
|
313
|
-
emitTrackingActionResult(this.
|
|
316
|
+
emitTrackingActionResult(this.eventCtx(), event, address, skipResult, trackingExtras);
|
|
314
317
|
return skipResult;
|
|
315
318
|
}
|
|
316
319
|
const withdrawable = clearinghouseView.withdrawable;
|
|
@@ -324,238 +327,301 @@ export class OpenPositionAction {
|
|
|
324
327
|
let submittedCount = 0;
|
|
325
328
|
let failedCount = 0;
|
|
326
329
|
for (const signal of sorted) {
|
|
327
|
-
// Telemetry: per-signal work window — every result entry below carries startedAt/endedAt.
|
|
328
|
-
const signalStartedAt = Date.now();
|
|
329
330
|
const asset = signal.asset;
|
|
330
331
|
const direction = signal.direction;
|
|
331
332
|
const signalScore = signal.score;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
333
|
+
await withSpan("signal.evaluation", {
|
|
334
|
+
"senpi.signal.asset": asset,
|
|
335
|
+
"senpi.signal.direction": direction,
|
|
336
|
+
...(typeof signalScore === "number" ? { "senpi.signal.score": signalScore } : {}),
|
|
337
|
+
}, async (span) => {
|
|
338
|
+
// Terminal outcome on this candidate's span. A failed result also shows red.
|
|
339
|
+
const outcome = (result, reason) => {
|
|
340
|
+
span.setAttribute("senpi.outcome.result", result);
|
|
341
|
+
span.setAttribute("senpi.outcome.reason_code", reason);
|
|
342
|
+
if (result === "failed")
|
|
343
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: reason });
|
|
344
|
+
};
|
|
345
|
+
// Telemetry: per-signal work window — every result entry below carries startedAt/endedAt.
|
|
346
|
+
const signalStartedAt = Date.now();
|
|
347
|
+
const signalDex = signal.dex;
|
|
348
|
+
const submittedDex = typeof signalDex === "string" && signalDex.trim() !== "" ? signalDex.trim() : "main";
|
|
349
|
+
const dexForLookup = submittedDex === "main" ? undefined : submittedDex;
|
|
350
|
+
if (remainingSlots <= 0) {
|
|
351
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: 'no_slots', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
352
|
+
outcome("skipped", 'no_slots');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!direction || (direction !== 'LONG' && direction !== 'SHORT')) {
|
|
356
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, success: false, reason: 'invalid_direction', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
357
|
+
failedCount++;
|
|
358
|
+
outcome("failed", 'invalid_direction');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (strategyState.hasOpenPosition(asset, dexForLookup)) {
|
|
362
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: 'position_already_exists', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
363
|
+
outcome("skipped", 'position_already_exists');
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// Risk guard per-open (real-time via MCP, with per-asset cooldown)
|
|
367
|
+
const gate = await withSpan("risk.gate", {}, async (gateSpan) => {
|
|
368
|
+
const g = await this.context.riskGuard.checkGate(address, { candidateAsset: asset });
|
|
369
|
+
gateSpan.setAttribute("senpi.risk.gate", g.gate);
|
|
370
|
+
if (g.gate !== 'OPEN')
|
|
371
|
+
gateSpan.setStatus({ code: SpanStatusCode.ERROR, message: g.reason ?? g.gate });
|
|
372
|
+
return g;
|
|
364
373
|
});
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
374
|
+
if (gate.gate !== 'OPEN') {
|
|
375
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: `risk_gate_${gate.gate}`, startedAt: signalStartedAt, endedAt: Date.now() });
|
|
376
|
+
outcome("skipped", `risk_gate_${gate.gate}`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// Leverage: LLM override > config-based resolution
|
|
380
|
+
const llmLeverage = typeof signal.leverage === 'number' ? signal.leverage : undefined;
|
|
381
|
+
const maxLeverage = await this.context.maxLeverageCache.get(asset, address) ?? 20;
|
|
382
|
+
const leverage = llmLeverage != null && llmLeverage > 0
|
|
383
|
+
? Math.max(MIN_LEVERAGE, Math.min(llmLeverage, maxLeverage))
|
|
384
|
+
: resolveLeverage({
|
|
385
|
+
tradingRisk: strategyState.config.tradingRisk,
|
|
386
|
+
maxLeverage,
|
|
387
|
+
defaultLeverage: strategyState.config.defaultLeverage,
|
|
388
|
+
leverageMultipliers: strategyState.config.leverageMultipliers,
|
|
389
|
+
});
|
|
390
|
+
// Margin: per-signal marginPct (% of withdrawable) > config margin_pct
|
|
391
|
+
// > margin_per_slot > budget / slots
|
|
392
|
+
const signalMarginPct = typeof signal.marginPct === 'number' ? signal.marginPct : undefined;
|
|
393
|
+
const { amount: marginAmount, source: marginSource } = resolveMarginWithSource({
|
|
382
394
|
signalMarginPct,
|
|
383
395
|
marginPct: strategyState.config.marginPct,
|
|
396
|
+
withdrawable,
|
|
384
397
|
marginPerSlot: strategyState.config.marginPerSlot,
|
|
385
398
|
budget: strategyState.config.budget,
|
|
386
399
|
slots: strategyState.config.slots,
|
|
387
|
-
withdrawable,
|
|
388
|
-
});
|
|
389
|
-
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, success: false, reason: 'no_margin_configured', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
390
|
-
failedCount++;
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
const notionalValue = marginAmount * leverage;
|
|
394
|
-
if (notionalValue < MIN_NOTIONAL_VALUE) {
|
|
395
|
-
logger.warn(`Skipping ${asset}: notional value $${notionalValue.toFixed(2)} ` +
|
|
396
|
-
`(margin $${marginAmount.toFixed(2)} × ${leverage}x) is below minimum $${MIN_NOTIONAL_VALUE}`);
|
|
397
|
-
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, notionalValue, success: false, reason: 'below_min_notional', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
398
|
-
failedCount++;
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
// Free-margin pre-flight: if we know free margin and it can't cover this
|
|
402
|
-
// position's margin, skip rather than fire a create the engine will reject
|
|
403
|
-
// (CREATE_INSUFFICIENT_FUNDS). Skip the check when free margin is unknown.
|
|
404
|
-
if (remainingMargin != null && remainingMargin < marginAmount) {
|
|
405
|
-
logger.warn(`Skipping ${asset}: insufficient free margin ` +
|
|
406
|
-
`($${remainingMargin.toFixed(2)} available < $${marginAmount.toFixed(2)} required)`);
|
|
407
|
-
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, requiredMarginAmount: marginAmount, availableMarginAmount: remainingMargin, success: true, reason: 'insufficient_margin', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
const signalReason = signal.reason ?? signal.signalType ?? reasoning;
|
|
411
|
-
try {
|
|
412
|
-
// Sizing breadcrumb: WHICH source set the margin + the numbers behind it,
|
|
413
|
-
// so a wrong size is attributable from logs without reverse-engineering.
|
|
414
|
-
logger.info('opening position', {
|
|
415
|
-
wallet: address,
|
|
416
|
-
asset,
|
|
417
|
-
direction,
|
|
418
|
-
source: marginSource,
|
|
419
|
-
withdrawable,
|
|
420
|
-
accountValue: clearinghouseView.accountValue,
|
|
421
|
-
marginAmount,
|
|
422
|
-
leverage,
|
|
423
|
-
notional: notionalValue,
|
|
424
400
|
});
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
401
|
+
// The applied percent — only meaningful for the two percent-of-withdrawable branches; the
|
|
402
|
+
// fixed/budget sources have no percent. Rides the result entry so the sizing attribution
|
|
403
|
+
// (source + percent) reaches the signal.outcome event, not just the structured log below.
|
|
404
|
+
const marginPct = marginSource === 'signal_marginPct' ? signalMarginPct
|
|
405
|
+
: marginSource === 'config_margin_pct' ? strategyState.config.marginPct
|
|
406
|
+
: undefined;
|
|
407
|
+
if (marginAmount == null || marginAmount <= 0) {
|
|
408
|
+
logger.warn(`Skipping ${asset}: no margin source resolved. ` +
|
|
409
|
+
`Set strategy.margin_per_slot, strategy.margin_pct, or strategy.budget + strategy.slots ` +
|
|
410
|
+
`in the recipe, or have the LLM provide a per-signal margin override.`, {
|
|
411
|
+
asset,
|
|
412
|
+
wallet: address,
|
|
413
|
+
signalMarginPct,
|
|
414
|
+
marginPct: strategyState.config.marginPct,
|
|
415
|
+
marginPerSlot: strategyState.config.marginPerSlot,
|
|
416
|
+
budget: strategyState.config.budget,
|
|
417
|
+
slots: strategyState.config.slots,
|
|
418
|
+
withdrawable,
|
|
419
|
+
});
|
|
420
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, success: false, reason: 'no_margin_configured', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
421
|
+
failedCount++;
|
|
422
|
+
outcome("failed", 'no_margin_configured');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const notionalValue = marginAmount * leverage;
|
|
426
|
+
if (notionalValue < MIN_NOTIONAL_VALUE) {
|
|
427
|
+
logger.warn(`Skipping ${asset}: notional value $${notionalValue.toFixed(2)} ` +
|
|
428
|
+
`(margin $${marginAmount.toFixed(2)} × ${leverage}x) is below minimum $${MIN_NOTIONAL_VALUE}`);
|
|
429
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, marginSource, marginPct, notionalValue, success: false, reason: 'below_min_notional', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
430
|
+
failedCount++;
|
|
431
|
+
outcome("failed", 'below_min_notional');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// Free-margin pre-flight: if we know free margin and it can't cover this
|
|
435
|
+
// position's margin, skip rather than fire a create the engine will reject
|
|
436
|
+
// (CREATE_INSUFFICIENT_FUNDS). Skip the check when free margin is unknown.
|
|
437
|
+
if (remainingMargin != null && remainingMargin < marginAmount) {
|
|
438
|
+
logger.warn(`Skipping ${asset}: insufficient free margin ` +
|
|
439
|
+
`($${remainingMargin.toFixed(2)} available < $${marginAmount.toFixed(2)} required)`);
|
|
440
|
+
results.push({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, marginSource, marginPct, requiredMarginAmount: marginAmount, availableMarginAmount: remainingMargin, success: true, reason: 'insufficient_margin', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
441
|
+
outcome("skipped", 'insufficient_margin');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const signalReason = signal.reason ?? signal.signalType ?? reasoning;
|
|
445
|
+
try {
|
|
446
|
+
// Sizing breadcrumb: WHICH source set the margin + the numbers behind it,
|
|
447
|
+
// so a wrong size is attributable from logs without reverse-engineering.
|
|
448
|
+
logger.info('opening position', {
|
|
449
|
+
wallet: address,
|
|
450
|
+
asset,
|
|
451
|
+
direction,
|
|
452
|
+
source: marginSource,
|
|
453
|
+
withdrawable,
|
|
454
|
+
accountValue: clearinghouseView.accountValue,
|
|
455
|
+
marginAmount,
|
|
456
|
+
leverage,
|
|
457
|
+
notional: notionalValue,
|
|
458
|
+
});
|
|
459
|
+
const result = await withSpan("mcp.createPosition", { "senpi.signal.asset": asset, "senpi.signal.direction": direction }, async (mcpSpan) => {
|
|
460
|
+
const r = await this.context.senpi.createPosition({
|
|
461
|
+
strategyWalletAddress: address,
|
|
462
|
+
orders: [
|
|
463
|
+
{
|
|
464
|
+
coin: asset,
|
|
465
|
+
direction,
|
|
466
|
+
leverage,
|
|
467
|
+
marginAmount,
|
|
468
|
+
orderType,
|
|
469
|
+
...(feeOptimizedLimitOptions ? { feeOptimizedLimitOptions } : {}),
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
reason: signalReason,
|
|
473
|
+
});
|
|
474
|
+
if (!r.success) {
|
|
475
|
+
const orderErrors = r.orders
|
|
476
|
+
.filter((o) => o.error)
|
|
477
|
+
.map((o) => `${o.coin}: ${o.error}`)
|
|
478
|
+
.join('; ');
|
|
479
|
+
const detail = r.error || orderErrors || 'unknown error';
|
|
480
|
+
mcpSpan.addEvent("order.failed");
|
|
481
|
+
mcpSpan.setStatus({ code: SpanStatusCode.ERROR, message: detail });
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
const o = r.orders[0];
|
|
485
|
+
if (o?.orderId != null)
|
|
486
|
+
mcpSpan.setAttribute("senpi.order.id", o.orderId);
|
|
487
|
+
if (o?.filled != null)
|
|
488
|
+
mcpSpan.setAttribute("senpi.outcome.filled", o.filled);
|
|
489
|
+
if (o?.executionAsMaker != null)
|
|
490
|
+
mcpSpan.setAttribute("senpi.outcome.execution_as_maker", o.executionAsMaker);
|
|
491
|
+
if (o?.entryPrice != null)
|
|
492
|
+
mcpSpan.setAttribute("senpi.order.entry_price", o.entryPrice);
|
|
493
|
+
if (o?.size != null)
|
|
494
|
+
mcpSpan.setAttribute("senpi.order.size", o.size);
|
|
495
|
+
mcpSpan.addEvent("order.placed");
|
|
496
|
+
if (o?.filled)
|
|
497
|
+
mcpSpan.addEvent("order.filled");
|
|
498
|
+
}
|
|
499
|
+
return r;
|
|
500
|
+
});
|
|
501
|
+
if (!result.success) {
|
|
502
|
+
const orderErrors = result.orders
|
|
503
|
+
.filter((o) => o.error)
|
|
504
|
+
.map((o) => `${o.coin}: ${o.error}`)
|
|
505
|
+
.join('; ');
|
|
506
|
+
const detail = result.error || orderErrors || 'unknown error';
|
|
507
|
+
results.push({
|
|
508
|
+
asset, direction, dex: submittedDex, score: signalScore,
|
|
509
|
+
leverage, marginAmount, marginSource, marginPct, orderType,
|
|
510
|
+
success: false, reason: 'position_open_failed', error: detail,
|
|
511
|
+
startedAt: signalStartedAt, endedAt: Date.now(),
|
|
512
|
+
});
|
|
513
|
+
// Task 8a: the venue rejected the open submit.
|
|
514
|
+
emitOrderFailed(this.context.hookSystem, event, address, {
|
|
515
|
+
asset,
|
|
516
|
+
direction,
|
|
517
|
+
reason: 'position_open_failed',
|
|
518
|
+
error: detail,
|
|
519
|
+
reduceOnly: false,
|
|
520
|
+
});
|
|
521
|
+
failedCount++;
|
|
522
|
+
outcome("failed", 'position_open_failed');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const submittedOrder = result.orders[0];
|
|
526
|
+
// Mint the position.id at intent — the moment the open order is submitted,
|
|
527
|
+
// before the position is confirmed open — so the id rides the action.open
|
|
528
|
+
// emit and the open trace correlates by position.id directly (no ~60s
|
|
529
|
+
// bridge on the read side).
|
|
530
|
+
let mintedPositionId;
|
|
531
|
+
if (this.context.positionIdRegistry) {
|
|
532
|
+
try {
|
|
533
|
+
const minted = this.context.positionIdRegistry.mintOnIntent(positionRegistryKey(address, submittedDex, asset));
|
|
534
|
+
mintedPositionId = minted.positionId;
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
// The order is already on the exchange — a registry failure must not land in the
|
|
538
|
+
// outer catch, which would mislabel this submit as an exception and skip the
|
|
539
|
+
// slot/margin bookkeeping.
|
|
540
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
541
|
+
logger.warn('position.id mint failed; continuing with submit emit', err, { asset });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
await this.context.hookSystem.fire({
|
|
545
|
+
type: HOOK_EVENT.ON_OPEN_ORDER_SUBMITTED,
|
|
546
|
+
runtimeName: event.runtimeName,
|
|
547
|
+
address,
|
|
548
|
+
data: {
|
|
549
|
+
asset,
|
|
430
550
|
direction,
|
|
431
551
|
leverage,
|
|
432
552
|
marginAmount,
|
|
433
553
|
orderType,
|
|
434
|
-
|
|
554
|
+
wallet: address,
|
|
555
|
+
dex: submittedDex,
|
|
556
|
+
reasoning: signalReason,
|
|
557
|
+
orderId: submittedOrder?.orderId,
|
|
558
|
+
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
559
|
+
size: submittedOrder?.size ?? null,
|
|
560
|
+
filled: submittedOrder?.filled ?? null,
|
|
561
|
+
__correlationId: event.data.__correlationId,
|
|
562
|
+
...(mintedPositionId ? { __positionId: mintedPositionId } : {}),
|
|
435
563
|
},
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
});
|
|
439
|
-
if (!result.success) {
|
|
440
|
-
const orderErrors = result.orders
|
|
441
|
-
.filter((o) => o.error)
|
|
442
|
-
.map((o) => `${o.coin}: ${o.error}`)
|
|
443
|
-
.join('; ');
|
|
444
|
-
const detail = result.error || orderErrors || 'unknown error';
|
|
564
|
+
timestamp: new Date().toISOString(),
|
|
565
|
+
});
|
|
445
566
|
results.push({
|
|
446
567
|
asset, direction, dex: submittedDex, score: signalScore,
|
|
447
|
-
leverage, marginAmount, orderType,
|
|
448
|
-
|
|
568
|
+
leverage, marginAmount, marginSource, marginPct, orderType,
|
|
569
|
+
orderId: submittedOrder?.orderId ?? null,
|
|
570
|
+
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
571
|
+
size: submittedOrder?.size ?? null,
|
|
572
|
+
filled: submittedOrder?.filled ?? null,
|
|
573
|
+
success: true, reason: 'submitted',
|
|
449
574
|
startedAt: signalStartedAt, endedAt: Date.now(),
|
|
450
575
|
});
|
|
451
|
-
// Task 8a
|
|
452
|
-
|
|
576
|
+
// Task 8a order-event log (additive, fire-and-forget — see emit helpers). The open submit
|
|
577
|
+
// is an `order.placed`; a confirmed fill additionally emits `order.filled`.
|
|
578
|
+
emitOrderPlaced(this.context.hookSystem, event, address, {
|
|
579
|
+
orderId: submittedOrder?.orderId,
|
|
580
|
+
orderType,
|
|
453
581
|
asset,
|
|
454
582
|
direction,
|
|
455
|
-
|
|
456
|
-
error: detail,
|
|
583
|
+
size: submittedOrder?.size,
|
|
457
584
|
reduceOnly: false,
|
|
585
|
+
...(mintedPositionId ? { __positionId: mintedPositionId } : {}),
|
|
458
586
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (this.context.positionIdRegistry) {
|
|
469
|
-
try {
|
|
470
|
-
const minted = this.context.positionIdRegistry.mintOnIntent(positionRegistryKey(address, submittedDex, asset));
|
|
471
|
-
mintedPositionId = minted.positionId;
|
|
472
|
-
}
|
|
473
|
-
catch (err) {
|
|
474
|
-
// The order is already on the exchange — a registry failure must not land in the
|
|
475
|
-
// outer catch, which would mislabel this submit as an exception and skip the
|
|
476
|
-
// slot/margin bookkeeping.
|
|
477
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
478
|
-
logger.warn('position.id mint failed; continuing with submit emit', { asset, err: message });
|
|
587
|
+
if (submittedOrder?.filled) {
|
|
588
|
+
emitOrderFilled(this.context.hookSystem, event, address, {
|
|
589
|
+
orderId: submittedOrder?.orderId,
|
|
590
|
+
asset,
|
|
591
|
+
fillPrice: submittedOrder?.entryPrice,
|
|
592
|
+
fillSize: submittedOrder?.size,
|
|
593
|
+
executionAsMaker: submittedOrder?.executionAsMaker,
|
|
594
|
+
...(mintedPositionId ? { __positionId: mintedPositionId } : {}),
|
|
595
|
+
});
|
|
479
596
|
}
|
|
597
|
+
submittedCount++;
|
|
598
|
+
remainingSlots--;
|
|
599
|
+
if (remainingMargin != null)
|
|
600
|
+
remainingMargin -= marginAmount;
|
|
601
|
+
outcome("submitted", 'submitted');
|
|
480
602
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
603
|
+
catch (error) {
|
|
604
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
605
|
+
logger.error('open failed', error, { asset });
|
|
606
|
+
results.push({
|
|
607
|
+
asset, direction, dex: submittedDex, score: signalScore,
|
|
608
|
+
leverage, marginAmount, marginSource, marginPct, orderType,
|
|
609
|
+
success: false, reason: 'exception', error: message,
|
|
610
|
+
startedAt: signalStartedAt, endedAt: Date.now(),
|
|
611
|
+
});
|
|
612
|
+
// Task 8a: the open submit threw.
|
|
613
|
+
emitOrderFailed(this.context.hookSystem, event, address, {
|
|
486
614
|
asset,
|
|
487
615
|
direction,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
dex: submittedDex,
|
|
493
|
-
reasoning: signalReason,
|
|
494
|
-
orderId: submittedOrder?.orderId,
|
|
495
|
-
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
496
|
-
size: submittedOrder?.size ?? null,
|
|
497
|
-
filled: submittedOrder?.filled ?? null,
|
|
498
|
-
__correlationId: event.data.__correlationId,
|
|
499
|
-
...(mintedPositionId ? { __positionId: mintedPositionId } : {}),
|
|
500
|
-
},
|
|
501
|
-
timestamp: new Date().toISOString(),
|
|
502
|
-
});
|
|
503
|
-
results.push({
|
|
504
|
-
asset, direction, dex: submittedDex, score: signalScore,
|
|
505
|
-
leverage, marginAmount, orderType,
|
|
506
|
-
orderId: submittedOrder?.orderId ?? null,
|
|
507
|
-
entryPrice: submittedOrder?.entryPrice ?? null,
|
|
508
|
-
size: submittedOrder?.size ?? null,
|
|
509
|
-
filled: submittedOrder?.filled ?? null,
|
|
510
|
-
success: true, reason: 'submitted',
|
|
511
|
-
startedAt: signalStartedAt, endedAt: Date.now(),
|
|
512
|
-
});
|
|
513
|
-
// Task 8a order-event log (additive, fire-and-forget — see emit helpers). The open submit
|
|
514
|
-
// is an `order.placed`; a confirmed fill additionally emits `order.filled`.
|
|
515
|
-
emitOrderPlaced(this.context.hookSystem, event, address, {
|
|
516
|
-
orderId: submittedOrder?.orderId,
|
|
517
|
-
orderType,
|
|
518
|
-
asset,
|
|
519
|
-
direction,
|
|
520
|
-
size: submittedOrder?.size,
|
|
521
|
-
reduceOnly: false,
|
|
522
|
-
...(mintedPositionId ? { __positionId: mintedPositionId } : {}),
|
|
523
|
-
});
|
|
524
|
-
if (submittedOrder?.filled) {
|
|
525
|
-
emitOrderFilled(this.context.hookSystem, event, address, {
|
|
526
|
-
orderId: submittedOrder?.orderId,
|
|
527
|
-
asset,
|
|
528
|
-
fillPrice: submittedOrder?.entryPrice,
|
|
529
|
-
fillSize: submittedOrder?.size,
|
|
530
|
-
executionAsMaker: submittedOrder?.executionAsMaker,
|
|
531
|
-
...(mintedPositionId ? { __positionId: mintedPositionId } : {}),
|
|
616
|
+
reason: 'exception',
|
|
617
|
+
error: message,
|
|
618
|
+
errorName: error instanceof Error ? error.name : undefined,
|
|
619
|
+
reduceOnly: false,
|
|
532
620
|
});
|
|
621
|
+
failedCount++;
|
|
622
|
+
outcome("failed", 'exception');
|
|
533
623
|
}
|
|
534
|
-
|
|
535
|
-
remainingSlots--;
|
|
536
|
-
if (remainingMargin != null)
|
|
537
|
-
remainingMargin -= marginAmount;
|
|
538
|
-
}
|
|
539
|
-
catch (error) {
|
|
540
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
541
|
-
logger.error('open failed', { asset, err: message });
|
|
542
|
-
results.push({
|
|
543
|
-
asset, direction, dex: submittedDex, score: signalScore,
|
|
544
|
-
leverage, marginAmount, orderType,
|
|
545
|
-
success: false, reason: 'exception', error: message,
|
|
546
|
-
startedAt: signalStartedAt, endedAt: Date.now(),
|
|
547
|
-
});
|
|
548
|
-
// Task 8a: the open submit threw.
|
|
549
|
-
emitOrderFailed(this.context.hookSystem, event, address, {
|
|
550
|
-
asset,
|
|
551
|
-
direction,
|
|
552
|
-
reason: 'exception',
|
|
553
|
-
error: message,
|
|
554
|
-
errorName: error instanceof Error ? error.name : undefined,
|
|
555
|
-
reduceOnly: false,
|
|
556
|
-
});
|
|
557
|
-
failedCount++;
|
|
558
|
-
}
|
|
624
|
+
});
|
|
559
625
|
}
|
|
560
626
|
const skippedCount = results.length - submittedCount - failedCount;
|
|
561
627
|
const actionResult = {
|
|
@@ -572,7 +638,7 @@ export class OpenPositionAction {
|
|
|
572
638
|
_decisionPrompt: decisionPrompt ?? null,
|
|
573
639
|
},
|
|
574
640
|
};
|
|
575
|
-
emitTrackingActionResult(this.
|
|
641
|
+
emitTrackingActionResult(this.eventCtx(), event, address, actionResult, trackingExtras);
|
|
576
642
|
return actionResult;
|
|
577
643
|
}
|
|
578
644
|
}
|