@senpi/runtime 3.0.108 → 3.0.111
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/action-events.d.ts +21 -7
- package/dist/actions/action-events.d.ts.map +1 -1
- package/dist/actions/action-events.js +54 -26
- package/dist/actions/action-events.js.map +1 -1
- package/dist/actions/action-runtime-module.d.ts.map +1 -1
- package/dist/actions/action-runtime-module.js +38 -7
- package/dist/actions/action-runtime-module.js.map +1 -1
- package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
- package/dist/actions/close-position/close-position.action.js +122 -15
- 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 +15 -2
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/event-derivation.d.ts +71 -2
- package/dist/actions/event-derivation.d.ts.map +1 -1
- package/dist/actions/event-derivation.js +251 -8
- package/dist/actions/event-derivation.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts +28 -2
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +220 -52
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.d.ts +2 -1
- package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.js +62 -17
- package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
- package/dist/actions/skipped-signal-results.d.ts +25 -0
- package/dist/actions/skipped-signal-results.d.ts.map +1 -0
- package/dist/actions/skipped-signal-results.js +71 -0
- package/dist/actions/skipped-signal-results.js.map +1 -0
- package/dist/actions/store/types.d.ts +9 -2
- package/dist/actions/store/types.d.ts.map +1 -1
- package/dist/actions/store/types.js.map +1 -1
- package/dist/actions/types.d.ts +13 -0
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +13 -0
- package/dist/actions/types.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +3 -0
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +15 -0
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +15 -0
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/engine-client/event-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/event-mapping.js +14 -0
- package/dist/dsl/engine-client/event-mapping.js.map +1 -1
- package/dist/dsl/events/build-closed-event.d.ts +16 -1
- package/dist/dsl/events/build-closed-event.d.ts.map +1 -1
- package/dist/dsl/events/build-closed-event.js +6 -0
- package/dist/dsl/events/build-closed-event.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +7 -0
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +37 -7
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +6 -2
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +15 -9
- package/dist/dsl/monitor/create-monitor.js.map +1 -1
- package/dist/dsl/monitor/dsl-failure-events.d.ts +2 -0
- package/dist/dsl/monitor/dsl-failure-events.d.ts.map +1 -1
- package/dist/dsl/monitor/dsl-failure-events.js +2 -1
- package/dist/dsl/monitor/dsl-failure-events.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts +1 -0
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +72 -29
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +8 -2
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +8 -6
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/health/types.d.ts +13 -0
- package/dist/health/types.d.ts.map +1 -1
- package/dist/health/types.js +12 -0
- package/dist/health/types.js.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -39
- package/dist/index.js.map +1 -1
- package/dist/runtime/action-registry.d.ts.map +1 -1
- package/dist/runtime/action-registry.js +4 -3
- package/dist/runtime/action-registry.js.map +1 -1
- package/dist/runtime/domain-events.d.ts.map +1 -1
- package/dist/runtime/domain-events.js +7 -0
- package/dist/runtime/domain-events.js.map +1 -1
- package/dist/runtime/lifecycle-events.d.ts +9 -1
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js +11 -2
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/reconcile-run.d.ts +13 -3
- package/dist/runtime/reconcile-run.d.ts.map +1 -1
- package/dist/runtime/reconcile-run.js +4 -3
- package/dist/runtime/reconcile-run.js.map +1 -1
- package/dist/runtime/reconcile-state.d.ts +8 -0
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +4 -1
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/run.d.ts +1 -0
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runtime-schema.js +3 -3
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +27 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.d.ts +13 -0
- package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js +1 -0
- package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
- package/dist/runtime/validate-dsl-position-tracking.d.ts.map +1 -1
- package/dist/runtime/validate-dsl-position-tracking.js +2 -1
- package/dist/runtime/validate-dsl-position-tracking.js.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.js +1 -0
- package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
- package/dist/runtime-api/handlers/types.d.ts +5 -0
- package/dist/runtime-api/handlers/types.d.ts.map +1 -1
- package/dist/runtime-api/intake/ingest-request.d.ts +17 -0
- package/dist/runtime-api/intake/ingest-request.d.ts.map +1 -0
- package/dist/runtime-api/intake/ingest-request.js +34 -0
- package/dist/runtime-api/intake/ingest-request.js.map +1 -0
- package/dist/runtime-api/intake/intake-events.d.ts +1 -1
- package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-events.js +9 -3
- package/dist/runtime-api/intake/intake-events.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +4 -19
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +15 -0
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +6 -0
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.js +2 -1
- package/dist/runtime-api/routes/signals.js.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +1 -0
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +6 -0
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/scanners/engine/engine.d.ts +22 -1
- package/dist/scanners/engine/engine.d.ts.map +1 -1
- package/dist/scanners/engine/engine.js +78 -11
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/engine/lifecycle.d.ts +7 -1
- package/dist/scanners/engine/lifecycle.d.ts.map +1 -1
- package/dist/scanners/events.d.ts +4 -0
- package/dist/scanners/events.d.ts.map +1 -1
- package/dist/scanners/events.js +2 -0
- package/dist/scanners/events.js.map +1 -1
- package/dist/scanners/external/scaffold/delivery.py +136 -14
- package/dist/scanners/external/scaffold/scaffold.py +61 -8
- package/dist/scanners/external/scaffold/ulid.py +58 -0
- package/dist/scanners/external-scanner-receiver.d.ts +17 -0
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +8 -0
- package/dist/scanners/external-scanner-receiver.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts +1 -0
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +59 -9
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/scripts/run-dsl-standalone-mock.js +6 -2
- package/dist/scripts/run-dsl-standalone-mock.js.map +1 -1
- package/dist/senpi/client.d.ts +16 -3
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +44 -13
- package/dist/senpi/client.js.map +1 -1
- package/dist/strategy/strategy-state.d.ts +9 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/strategy/strategy-state.js +7 -2
- package/dist/strategy/strategy-state.js.map +1 -1
- package/dist/telemetry/attributes.d.ts +20 -0
- package/dist/telemetry/attributes.d.ts.map +1 -1
- package/dist/telemetry/attributes.js +24 -0
- package/dist/telemetry/attributes.js.map +1 -1
- package/dist/telemetry/event-builders.d.ts +81 -6
- package/dist/telemetry/event-builders.d.ts.map +1 -1
- package/dist/telemetry/event-builders.js +141 -17
- package/dist/telemetry/event-builders.js.map +1 -1
- package/dist/telemetry/event-store.d.ts +6 -0
- package/dist/telemetry/event-store.d.ts.map +1 -1
- package/dist/telemetry/event-store.js +12 -8
- package/dist/telemetry/event-store.js.map +1 -1
- package/dist/telemetry/live-telemetry.d.ts +192 -0
- package/dist/telemetry/live-telemetry.d.ts.map +1 -0
- package/dist/telemetry/live-telemetry.js +676 -0
- package/dist/telemetry/live-telemetry.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +14 -2
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +19 -4
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/scanner-stats.d.ts +143 -0
- package/dist/telemetry/scanner-stats.d.ts.map +1 -0
- package/dist/telemetry/scanner-stats.js +426 -0
- package/dist/telemetry/scanner-stats.js.map +1 -0
- package/dist/telemetry/spans/redact-pii.d.ts.map +1 -1
- package/dist/telemetry/spans/redact-pii.js +18 -2
- package/dist/telemetry/spans/redact-pii.js.map +1 -1
- package/dist/telemetry/strategy-topology.d.ts +99 -0
- package/dist/telemetry/strategy-topology.d.ts.map +1 -0
- package/dist/telemetry/strategy-topology.js +93 -0
- package/dist/telemetry/strategy-topology.js.map +1 -0
- package/dist/telemetry/telemetry-publisher.d.ts +63 -0
- package/dist/telemetry/telemetry-publisher.d.ts.map +1 -0
- package/dist/telemetry/telemetry-publisher.js +229 -0
- package/dist/telemetry/telemetry-publisher.js.map +1 -0
- package/dist/telemetry/timeline-buffer.d.ts +95 -0
- package/dist/telemetry/timeline-buffer.d.ts.map +1 -0
- package/dist/telemetry/timeline-buffer.js +237 -0
- package/dist/telemetry/timeline-buffer.js.map +1 -0
- package/dist/telemetry/tracing.d.ts +9 -0
- package/dist/telemetry/tracing.d.ts.map +1 -1
- package/dist/telemetry/tracing.js +36 -0
- package/dist/telemetry/tracing.js.map +1 -1
- package/dist/types/dsl/index.d.ts +11 -0
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/types/scanner.d.ts +15 -0
- package/dist/types/scanner.d.ts.map +1 -1
- package/dist/types/strategy.d.ts +5 -1
- package/dist/types/strategy.d.ts.map +1 -1
- package/dist/utils/event-catalog.d.ts +166 -0
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +18 -0
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/utils/logger.d.ts +4 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/telemetry-required-log.d.ts +27 -0
- package/dist/utils/telemetry-required-log.d.ts.map +1 -0
- package/dist/utils/telemetry-required-log.js +68 -0
- package/dist/utils/telemetry-required-log.js.map +1 -0
- package/dist/utils/ulid.d.ts +9 -0
- package/dist/utils/ulid.d.ts.map +1 -0
- package/dist/utils/ulid.js +43 -0
- package/dist/utils/ulid.js.map +1 -0
- package/dist/validate/recipe-checks.d.ts.map +1 -1
- package/dist/validate/recipe-checks.js +2 -2
- package/dist/validate/recipe-checks.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ACTION_TYPE } from '../types.js';
|
|
1
2
|
import { decisionMetaFromDecision } from '../decision-meta.js';
|
|
2
3
|
import { DecisionEngine, LlmDecisionStrategy, NoDecisionStrategy, RuleDecisionStrategy } from '../decision-engine.js';
|
|
3
4
|
import { HOOK_EVENT } from '../../types/hooks.js';
|
|
@@ -6,21 +7,57 @@ import { emitDecisionErrorNotification } from '../decision-error-notification.js
|
|
|
6
7
|
import { emitEvent } from '../event-derivation.js';
|
|
7
8
|
import { buildRuntimePausedEvent } from '../../runtime/lifecycle-events.js';
|
|
8
9
|
import { resolveLeverage } from '../../leverage/resolve-leverage.js';
|
|
9
|
-
import { SenpiTimeoutError } from '../../senpi/client.js';
|
|
10
|
+
import { SenpiTimeoutError, isOutcomeUnknownCode } from '../../senpi/client.js';
|
|
10
11
|
import { resolveMarginWithSource } from './resolve-margin.js';
|
|
11
12
|
import { createLogger } from '../../utils/logger.js';
|
|
12
13
|
import { positionRegistryKey } from '../../runtime/position-id-registry.js';
|
|
13
14
|
import { resolveOpenOrderType, resolveFeeOptimizedLimitOptions } from '../../utils/resolve-order-type.js';
|
|
14
15
|
import { emitActionResultEvents, emitOrderPlaced, emitOrderFilled, emitOrderFailed, buildActionEventContext } from '../action-events.js';
|
|
16
|
+
import { skippedSignalResults } from '../skipped-signal-results.js';
|
|
15
17
|
import { withSpan } from '../../telemetry/tracing.js';
|
|
16
18
|
import { SpanStatusCode } from '@opentelemetry/api';
|
|
17
19
|
import { classifyToolError } from '../../senpi/tool-errors.js';
|
|
18
20
|
import { hasOpenPositionIn } from '../../strategy/strategy-state.js';
|
|
19
21
|
import { createPendingOpensLedger, pendingOpenKey } from '../../runtime/pending-opens-ledger.js';
|
|
20
|
-
import { recordSignalOutcome } from '../../telemetry/metrics.js';
|
|
21
22
|
const logger = createLogger("[actions][open-position]");
|
|
22
23
|
/** Hyperliquid minimum notional value (margin × leverage) required to open a position. */
|
|
23
24
|
const MIN_NOTIONAL_VALUE = 10;
|
|
25
|
+
/**
|
|
26
|
+
* Wall-clock bound on the clearing-house reads this action makes.
|
|
27
|
+
*
|
|
28
|
+
* These reads run inside the runtime's `reconcile` key-lock, which the DSL monitor's own
|
|
29
|
+
* reconcile waits on. The client's retry ladder bounds attempt COUNT, not time — three 45s
|
|
30
|
+
* attempts plus backoff is ~2.5 min of held lock, and it holds exactly when the MCP is
|
|
31
|
+
* degraded, which is when the exit engine can least afford to wait. 20s sits under the DSL
|
|
32
|
+
* tick's 30s default (a recipe on a faster tick loses at most one tick) while leaving a
|
|
33
|
+
* healthy read, which lands in 1–5s, several attempts of headroom.
|
|
34
|
+
*/
|
|
35
|
+
export const CLEARINGHOUSE_READ_TIMEOUT_MS = 20_000;
|
|
36
|
+
/**
|
|
37
|
+
* A deadline for one clearing-house read. `AbortSignal.timeout` would do, but its timer is
|
|
38
|
+
* invisible to test fake-timers and never cleared — this one is both controllable and
|
|
39
|
+
* cancelled on the happy path, so a bounded read leaves nothing pending behind it.
|
|
40
|
+
*/
|
|
41
|
+
function readDeadline(ms) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
controller.abort(new Error(`clearinghouse read exceeded ${ms}ms`));
|
|
45
|
+
}, ms);
|
|
46
|
+
return { signal: controller.signal, clear: () => clearTimeout(timer) };
|
|
47
|
+
}
|
|
48
|
+
/** Run one clearing-house read under {@link CLEARINGHOUSE_READ_TIMEOUT_MS}. Fails closed. */
|
|
49
|
+
async function readBookBounded(strategyState) {
|
|
50
|
+
const deadline = readDeadline(CLEARINGHOUSE_READ_TIMEOUT_MS);
|
|
51
|
+
try {
|
|
52
|
+
return await strategyState.fetchClearinghouseState({
|
|
53
|
+
retryUnreadable: true,
|
|
54
|
+
signal: deadline.signal,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
deadline.clear();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
24
61
|
/** Minimum leverage floor — positions cannot go below 1x. */
|
|
25
62
|
const MIN_LEVERAGE = 1;
|
|
26
63
|
/**
|
|
@@ -41,7 +78,7 @@ export class OpenPositionAction {
|
|
|
41
78
|
config;
|
|
42
79
|
context;
|
|
43
80
|
name;
|
|
44
|
-
actionType =
|
|
81
|
+
actionType = ACTION_TYPE.openPosition;
|
|
45
82
|
decisionEngine;
|
|
46
83
|
/**
|
|
47
84
|
* Ledger of last resort for a context that carries none (standalone scripts, older call
|
|
@@ -124,13 +161,23 @@ export class OpenPositionAction {
|
|
|
124
161
|
// re-submitting on every signal.
|
|
125
162
|
if (this.context.backendPauseRegistry?.isPaused(address)) {
|
|
126
163
|
logger.warn('Skipping signals: strategy is paused or closed on the backend', { wallet: address });
|
|
127
|
-
|
|
164
|
+
const pausedResults = skippedSignalResults(scanResult.signals, 'strategy_backend_paused', Date.now());
|
|
165
|
+
const pausedResult = {
|
|
128
166
|
success: true,
|
|
129
167
|
action_type: this.actionType,
|
|
130
168
|
summary: 'Strategy is paused or closed on the backend — skipping signals until it is resumed',
|
|
131
169
|
skipped: true,
|
|
132
170
|
skipReason: 'strategy_backend_paused',
|
|
171
|
+
data: {
|
|
172
|
+
results: pausedResults,
|
|
173
|
+
submittedCount: 0,
|
|
174
|
+
failedCount: 0,
|
|
175
|
+
skippedCount: pausedResults.length,
|
|
176
|
+
},
|
|
133
177
|
};
|
|
178
|
+
// No decision ran yet, so the pass carries no `_decisionMeta` and yields no `decision.made`.
|
|
179
|
+
emitActionResultEvents(this.eventCtx(), event, address, pausedResult, { scanResult });
|
|
180
|
+
return pausedResult;
|
|
134
181
|
}
|
|
135
182
|
// The exchange view for THIS pass. Read here, once, and threaded through the rest of it.
|
|
136
183
|
//
|
|
@@ -140,9 +187,11 @@ export class OpenPositionAction {
|
|
|
140
187
|
// froze the slot gate, the dedupe check and the margin budget at whatever the first
|
|
141
188
|
// signal after boot happened to see: a full snapshot skipped every later signal forever,
|
|
142
189
|
// an empty one let the slot cap and the dedupe never bind at all.
|
|
143
|
-
const view = await
|
|
190
|
+
const view = await this.readClearinghouseView(strategyState, address);
|
|
144
191
|
if (view === undefined) {
|
|
145
|
-
return this.skipClearinghouseUnknown(event, address,
|
|
192
|
+
return this.skipClearinghouseUnknown(event, address,
|
|
193
|
+
// No decision has run, so the scan's own signals are the only attributable inputs.
|
|
194
|
+
skippedSignalResults(scanResult.signals, 'clearinghouse_unknown', Date.now()), undefined, undefined, undefined, { scanResult });
|
|
146
195
|
}
|
|
147
196
|
const snapshot = { view, fetchedAt: Date.now() };
|
|
148
197
|
// Early exit: no slots. Counted as snapshot positions PLUS opens this runtime has already
|
|
@@ -163,19 +212,30 @@ export class OpenPositionAction {
|
|
|
163
212
|
const slots = strategyState.config.slots;
|
|
164
213
|
const openCount = view.positions.length + pending.length;
|
|
165
214
|
if (openCount >= slots) {
|
|
215
|
+
// The log is edge-triggered and rate-limited: a full book on a high-signal-rate strategy
|
|
216
|
+
// is the loudest path there is, and the fact is one about the strategy, not one per
|
|
217
|
+
// candidate. The per-signal rows below are the event plane, which is bounded per pass.
|
|
166
218
|
this.logBookFull(address, openCount, slots, pending.map((p) => p.coin), view.positions);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// fact about the strategy, not one per candidate. The counter still carries it.
|
|
170
|
-
recordSignalOutcome({ result: 'skipped', reasonCode: 'no_slots' });
|
|
171
|
-
return {
|
|
219
|
+
const noSlotResults = skippedSignalResults(scanResult.signals, 'no_slots', Date.now());
|
|
220
|
+
const noSlotResult = {
|
|
172
221
|
success: true,
|
|
173
222
|
action_type: this.actionType,
|
|
174
223
|
summary: `No slots available (${openCount}/${slots} open)`,
|
|
175
224
|
skipped: true,
|
|
176
225
|
skipReason: 'no_slots',
|
|
177
|
-
data: {
|
|
226
|
+
data: {
|
|
227
|
+
results: noSlotResults,
|
|
228
|
+
submittedCount: 0,
|
|
229
|
+
failedCount: 0,
|
|
230
|
+
skippedCount: noSlotResults.length,
|
|
231
|
+
openCount,
|
|
232
|
+
slots,
|
|
233
|
+
pendingCount: pending.length,
|
|
234
|
+
},
|
|
178
235
|
};
|
|
236
|
+
// No decision ran yet, so the pass carries no `_decisionMeta` and yields no `decision.made`.
|
|
237
|
+
emitActionResultEvents(this.eventCtx(), event, address, noSlotResult, { scanResult });
|
|
238
|
+
return noSlotResult;
|
|
179
239
|
}
|
|
180
240
|
// A pass with room re-arms the edge, so the next time the book fills it is logged again.
|
|
181
241
|
this.noSlotsLog.full = false;
|
|
@@ -190,17 +250,43 @@ export class OpenPositionAction {
|
|
|
190
250
|
? (address, scannerId) => this.context.scannerModule.getScannerContext(address, scannerId)
|
|
191
251
|
: undefined,
|
|
192
252
|
};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
253
|
+
// A context provider or the prompt template can throw before any decision exists. Both are
|
|
254
|
+
// preparation for the same decision, so they fail as one unit: the scan signals are recorded as
|
|
255
|
+
// failures and the pass returns, instead of the throw escaping to the caller unrecorded.
|
|
256
|
+
let contextData;
|
|
257
|
+
let interpolatedPrompt;
|
|
258
|
+
try {
|
|
259
|
+
contextData = this.config.context?.length
|
|
260
|
+
? await this.context.contextBuilder.build(this.config.context, providerRequest)
|
|
261
|
+
: {};
|
|
262
|
+
logger.info('context data', contextData);
|
|
263
|
+
// Interpolate prompt template with resolved context + action params
|
|
264
|
+
interpolatedPrompt = this.config.decision_prompt
|
|
265
|
+
? interpolatePrompt(this.config.decision_prompt, {
|
|
266
|
+
...contextData,
|
|
267
|
+
...(this.config.params ?? {}),
|
|
268
|
+
})
|
|
269
|
+
: undefined;
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
273
|
+
logger.error('context build failed', error, { wallet: address });
|
|
274
|
+
const contextErrorResults = skippedSignalResults(scanResult.signals, 'context_error', Date.now(), { success: false, error: message });
|
|
275
|
+
const contextErrorResult = {
|
|
276
|
+
success: false,
|
|
277
|
+
action_type: this.actionType,
|
|
278
|
+
summary: `Context build failed: ${message}`,
|
|
279
|
+
error: message,
|
|
280
|
+
data: {
|
|
281
|
+
results: contextErrorResults,
|
|
282
|
+
submittedCount: 0,
|
|
283
|
+
failedCount: contextErrorResults.length,
|
|
284
|
+
skippedCount: 0,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
emitActionResultEvents(this.eventCtx(), event, address, contextErrorResult, { scanResult });
|
|
288
|
+
return contextErrorResult;
|
|
289
|
+
}
|
|
204
290
|
// Get decision
|
|
205
291
|
let decision;
|
|
206
292
|
try {
|
|
@@ -216,12 +302,20 @@ export class OpenPositionAction {
|
|
|
216
302
|
const message = error instanceof Error ? error.message : String(error);
|
|
217
303
|
await emitDecisionErrorNotification(this.context, message);
|
|
218
304
|
const eventCtx = this.eventCtx();
|
|
305
|
+
// The decision produced nothing usable, so the scan signals are the only attributable inputs.
|
|
306
|
+
// The rows are failures, not skips: `success: false` plus the error message, counted under
|
|
307
|
+
// `failedCount` so the pass does not read as benign on the span or the stored record.
|
|
308
|
+
const errorResults = skippedSignalResults(scanResult.signals, 'decision_error', Date.now(), { success: false, error: message });
|
|
219
309
|
const errorResult = {
|
|
220
310
|
success: false,
|
|
221
311
|
action_type: this.actionType,
|
|
222
312
|
summary: `Decision failed: ${message}`,
|
|
223
313
|
error: message,
|
|
224
314
|
data: {
|
|
315
|
+
results: errorResults,
|
|
316
|
+
submittedCount: 0,
|
|
317
|
+
failedCount: errorResults.length,
|
|
318
|
+
skippedCount: 0,
|
|
225
319
|
_decisionMeta: {
|
|
226
320
|
durationMs: 0,
|
|
227
321
|
tokensUsed: null,
|
|
@@ -245,6 +339,7 @@ export class OpenPositionAction {
|
|
|
245
339
|
// Check decision
|
|
246
340
|
if (!decision.execute) {
|
|
247
341
|
logger.info('decision no execute', decision);
|
|
342
|
+
const noExecuteResults = skippedSignalResults(scanResult.signals, 'decision_no_execute', Date.now());
|
|
248
343
|
const rejectedResult = {
|
|
249
344
|
success: true,
|
|
250
345
|
action_type: this.actionType,
|
|
@@ -252,6 +347,10 @@ export class OpenPositionAction {
|
|
|
252
347
|
skipped: true,
|
|
253
348
|
skipReason: 'decision_no_execute',
|
|
254
349
|
data: {
|
|
350
|
+
results: noExecuteResults,
|
|
351
|
+
submittedCount: 0,
|
|
352
|
+
failedCount: 0,
|
|
353
|
+
skippedCount: noExecuteResults.length,
|
|
255
354
|
confidence: decision.confidence,
|
|
256
355
|
reasoning: decision.reasoning,
|
|
257
356
|
_decisionMeta: decisionMetaFromDecision(this.config, decision),
|
|
@@ -264,6 +363,7 @@ export class OpenPositionAction {
|
|
|
264
363
|
// Check confidence threshold
|
|
265
364
|
if (isBelowConfidence) {
|
|
266
365
|
logger.info('confidence below threshold', { confidence: decision.confidence, threshold: minConfidence });
|
|
366
|
+
const belowConfResults = skippedSignalResults(scanResult.signals, 'below_confidence', Date.now());
|
|
267
367
|
const belowConfResult = {
|
|
268
368
|
success: true,
|
|
269
369
|
action_type: this.actionType,
|
|
@@ -271,6 +371,10 @@ export class OpenPositionAction {
|
|
|
271
371
|
skipped: true,
|
|
272
372
|
skipReason: 'below_confidence',
|
|
273
373
|
data: {
|
|
374
|
+
results: belowConfResults,
|
|
375
|
+
submittedCount: 0,
|
|
376
|
+
failedCount: 0,
|
|
377
|
+
skippedCount: belowConfResults.length,
|
|
274
378
|
confidence: decision.confidence,
|
|
275
379
|
threshold: minConfidence,
|
|
276
380
|
_decisionMeta: decisionMetaFromDecision(this.config, decision),
|
|
@@ -284,12 +388,23 @@ export class OpenPositionAction {
|
|
|
284
388
|
const signals = this.normalizeSignals(decision.payload);
|
|
285
389
|
const meta = decisionMetaFromDecision(this.config, decision);
|
|
286
390
|
if (!signals.length) {
|
|
391
|
+
// `success: false` plus the error code on each row, matching the batch result and its
|
|
392
|
+
// `decision_error`/`context_error` siblings: the decision returned something unusable, so
|
|
393
|
+
// the rows classify as errors rather than as a benign skip indistinguishable from no_slots.
|
|
394
|
+
const invalidPayloadResults = skippedSignalResults(scanResult.signals, 'invalid_payload', Date.now(), { success: false, error: 'invalid_payload' });
|
|
287
395
|
const invalidPayloadResult = {
|
|
288
396
|
success: false,
|
|
289
397
|
action_type: this.actionType,
|
|
290
398
|
summary: 'Invalid decision payload: no actionable signals',
|
|
291
399
|
error: 'invalid_payload',
|
|
292
|
-
data: {
|
|
400
|
+
data: {
|
|
401
|
+
results: invalidPayloadResults,
|
|
402
|
+
submittedCount: 0,
|
|
403
|
+
failedCount: invalidPayloadResults.length,
|
|
404
|
+
skippedCount: 0,
|
|
405
|
+
_decisionMeta: meta,
|
|
406
|
+
_decisionPrompt: interpolatedPrompt ?? null,
|
|
407
|
+
},
|
|
293
408
|
};
|
|
294
409
|
emitActionResultEvents(eventCtx, event, address, invalidPayloadResult, trackingExtras);
|
|
295
410
|
return invalidPayloadResult;
|
|
@@ -300,6 +415,27 @@ export class OpenPositionAction {
|
|
|
300
415
|
pendingOpens() {
|
|
301
416
|
return this.context.pendingOpensLedger ?? this.ownPendingOpens;
|
|
302
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* One clearinghouse read, fail-closed on both an unreadable response and a throw. A throw is
|
|
420
|
+
* the same situation as an unreadable response — an unknown book — so it resolves undefined
|
|
421
|
+
* and reaches the skip below rather than escaping the action with no per-signal outcome
|
|
422
|
+
* recorded. Shared by the gate at the top of `handle` and the stale-view refetch in the
|
|
423
|
+
* signal loop.
|
|
424
|
+
*/
|
|
425
|
+
/**
|
|
426
|
+
* The bounded book read, failing closed. A read that throws — including one the deadline
|
|
427
|
+
* aborts — leaves the exchange view unknown, which the callers treat as a skip rather than
|
|
428
|
+
* trading against numbers nobody read.
|
|
429
|
+
*/
|
|
430
|
+
async readClearinghouseView(strategyState, address) {
|
|
431
|
+
try {
|
|
432
|
+
return await readBookBounded(strategyState);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
logger.warn('Clearinghouse fetch threw; treating the exchange view as unknown', error, { wallet: address });
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
303
439
|
/**
|
|
304
440
|
* Edge-triggered full-book log: once on the transition into full, then at most once per
|
|
305
441
|
* {@link NO_SLOTS_LOG_INTERVAL_MS} with the count it suppressed in between.
|
|
@@ -336,18 +472,12 @@ export class OpenPositionAction {
|
|
|
336
472
|
* rather than risk a duplicate or an over-margined open.
|
|
337
473
|
*
|
|
338
474
|
* Shared by both read sites (the gate at the top of `handle`, and the stale-view refetch in
|
|
339
|
-
* the signal loop).
|
|
340
|
-
*
|
|
475
|
+
* the signal loop). The rows come from the caller because the two sites attribute to
|
|
476
|
+
* different inputs: the scan's own signals before a decision exists, the decision's
|
|
477
|
+
* candidates after. `riskSnapshot` is undefined at the early site, where no gate snapshot
|
|
478
|
+
* has been taken yet.
|
|
341
479
|
*/
|
|
342
|
-
skipClearinghouseUnknown(event, address,
|
|
343
|
-
const skippedResults = sorted.map((signal) => ({
|
|
344
|
-
asset: signal.asset,
|
|
345
|
-
direction: signal.direction,
|
|
346
|
-
dex: typeof signal.dex === 'string' && signal.dex.trim() !== '' ? signal.dex.trim() : 'main',
|
|
347
|
-
score: signal.score,
|
|
348
|
-
success: true,
|
|
349
|
-
reason: 'clearinghouse_unknown',
|
|
350
|
-
}));
|
|
480
|
+
skipClearinghouseUnknown(event, address, skippedResults, meta, riskSnapshot, decisionPrompt, trackingExtras) {
|
|
351
481
|
logger.error('Skipping open: clearinghouse view unknown after retries', {
|
|
352
482
|
wallet: address,
|
|
353
483
|
skippedCount: skippedResults.length,
|
|
@@ -365,6 +495,7 @@ export class OpenPositionAction {
|
|
|
365
495
|
failedCount: 0,
|
|
366
496
|
skippedCount: skippedResults.length,
|
|
367
497
|
...(meta ? { _decisionMeta: meta } : {}),
|
|
498
|
+
...(riskSnapshot ? { _riskSnapshot: riskSnapshot } : {}),
|
|
368
499
|
_decisionPrompt: decisionPrompt ?? null,
|
|
369
500
|
},
|
|
370
501
|
};
|
|
@@ -416,12 +547,24 @@ export class OpenPositionAction {
|
|
|
416
547
|
// trusts is one it read itself, on this pass.
|
|
417
548
|
if (Date.now() - snapshot.fetchedAt <= CLEARINGHOUSE_VIEW_MAX_AGE_MS)
|
|
418
549
|
return snapshot.view;
|
|
419
|
-
return
|
|
550
|
+
return this.readClearinghouseView(strategyState, address);
|
|
420
551
|
})(),
|
|
421
552
|
]);
|
|
422
553
|
riskSnapshot = riskSnapResult;
|
|
423
554
|
if (clearinghouseView === undefined) {
|
|
424
|
-
|
|
555
|
+
const skippedResults = sorted.map((signal) => ({
|
|
556
|
+
asset: signal.asset,
|
|
557
|
+
direction: signal.direction,
|
|
558
|
+
dex: typeof signal.dex === 'string' && signal.dex.trim() !== '' ? signal.dex.trim() : 'main',
|
|
559
|
+
score: signal.score,
|
|
560
|
+
success: true,
|
|
561
|
+
reason: 'clearinghouse_unknown',
|
|
562
|
+
// Telemetry join back to this signal's scan entry — see `pushResult` in the signal loop.
|
|
563
|
+
...(typeof signal.__signalIndex === 'number' ? { __signalIndex: signal.__signalIndex } : {}),
|
|
564
|
+
}));
|
|
565
|
+
// The risk snapshot resolved alongside the view, so it is in hand here. An absent
|
|
566
|
+
// snapshot tells telemetry the gates never ran, which on this path is false.
|
|
567
|
+
return this.skipClearinghouseUnknown(event, address, skippedResults, meta, riskSnapshot, decisionPrompt, trackingExtras);
|
|
425
568
|
}
|
|
426
569
|
// ONE snapshot decides everything below: slots, dedupe and margin. Mixing a second read
|
|
427
570
|
// in mid-batch is how the counts stopped agreeing with each other in the first place.
|
|
@@ -465,16 +608,24 @@ export class OpenPositionAction {
|
|
|
465
608
|
};
|
|
466
609
|
// Telemetry: per-signal work window — every result entry below carries startedAt/endedAt.
|
|
467
610
|
const signalStartedAt = Date.now();
|
|
611
|
+
// Telemetry join: every row this signal produces carries its index into the scan's signal
|
|
612
|
+
// array, so the event mapper pairs the row with THIS signal's `meta`. Without it, two signals
|
|
613
|
+
// on the same asset in one pass would both be read as the first one. Absent on the LLM path
|
|
614
|
+
// (the model authors its own signals), where the mapper falls back to matching by asset.
|
|
615
|
+
const signalIndex = typeof signal.__signalIndex === 'number' ? signal.__signalIndex : undefined;
|
|
616
|
+
const pushResult = (row) => {
|
|
617
|
+
results.push(signalIndex !== undefined ? { ...row, __signalIndex: signalIndex } : row);
|
|
618
|
+
};
|
|
468
619
|
const signalDex = signal.dex;
|
|
469
620
|
const submittedDex = typeof signalDex === "string" && signalDex.trim() !== "" ? signalDex.trim() : "main";
|
|
470
621
|
const dexForLookup = submittedDex === "main" ? undefined : submittedDex;
|
|
471
622
|
if (remainingSlots <= 0) {
|
|
472
|
-
|
|
623
|
+
pushResult({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: 'no_slots', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
473
624
|
outcome("skipped", 'no_slots');
|
|
474
625
|
return;
|
|
475
626
|
}
|
|
476
627
|
if (!direction || (direction !== 'LONG' && direction !== 'SHORT')) {
|
|
477
|
-
|
|
628
|
+
pushResult({ asset, direction, dex: submittedDex, score: signalScore, success: false, reason: 'invalid_direction', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
478
629
|
failedCount++;
|
|
479
630
|
outcome("failed", 'invalid_direction');
|
|
480
631
|
return;
|
|
@@ -482,7 +633,7 @@ export class OpenPositionAction {
|
|
|
482
633
|
// Dedupe against THIS snapshot plus the in-flight opens it is too old to show.
|
|
483
634
|
const pendingKey = pendingOpenKey(asset, dexForLookup);
|
|
484
635
|
if (hasOpenPositionIn(view.positions, asset, dexForLookup) || pendingKeys.has(pendingKey)) {
|
|
485
|
-
|
|
636
|
+
pushResult({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: 'position_already_exists', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
486
637
|
outcome("skipped", 'position_already_exists');
|
|
487
638
|
return;
|
|
488
639
|
}
|
|
@@ -495,7 +646,7 @@ export class OpenPositionAction {
|
|
|
495
646
|
return g;
|
|
496
647
|
});
|
|
497
648
|
if (gate.gate !== 'OPEN') {
|
|
498
|
-
|
|
649
|
+
pushResult({ asset, direction, dex: submittedDex, score: signalScore, success: true, reason: `risk_gate_${gate.gate}`, startedAt: signalStartedAt, endedAt: Date.now() });
|
|
499
650
|
outcome("skipped", `risk_gate_${gate.gate}`);
|
|
500
651
|
return;
|
|
501
652
|
}
|
|
@@ -542,7 +693,7 @@ export class OpenPositionAction {
|
|
|
542
693
|
slots: strategyState.config.slots,
|
|
543
694
|
withdrawable,
|
|
544
695
|
});
|
|
545
|
-
|
|
696
|
+
pushResult({ asset, direction, dex: submittedDex, score: signalScore, leverage, success: false, reason: 'no_margin_configured', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
546
697
|
failedCount++;
|
|
547
698
|
outcome("failed", 'no_margin_configured');
|
|
548
699
|
return;
|
|
@@ -551,7 +702,7 @@ export class OpenPositionAction {
|
|
|
551
702
|
if (notionalValue < MIN_NOTIONAL_VALUE) {
|
|
552
703
|
logger.warn(`Skipping ${asset}: notional value $${notionalValue.toFixed(2)} ` +
|
|
553
704
|
`(margin $${marginAmount.toFixed(2)} × ${leverage}x) is below minimum $${MIN_NOTIONAL_VALUE}`);
|
|
554
|
-
|
|
705
|
+
pushResult({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, marginSource, marginPct, notionalValue, success: false, reason: 'below_min_notional', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
555
706
|
failedCount++;
|
|
556
707
|
outcome("failed", 'below_min_notional');
|
|
557
708
|
return;
|
|
@@ -562,14 +713,14 @@ export class OpenPositionAction {
|
|
|
562
713
|
if (remainingMargin != null && remainingMargin < marginAmount) {
|
|
563
714
|
logger.warn(`Skipping ${asset}: insufficient free margin ` +
|
|
564
715
|
`($${remainingMargin.toFixed(2)} available < $${marginAmount.toFixed(2)} required)`);
|
|
565
|
-
|
|
716
|
+
pushResult({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, marginSource, marginPct, requiredMarginAmount: marginAmount, availableMarginAmount: remainingMargin, success: true, reason: 'insufficient_margin', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
566
717
|
outcome("skipped", 'insufficient_margin');
|
|
567
718
|
return;
|
|
568
719
|
}
|
|
569
720
|
// A previous signal in this batch may have just armed the backend pause —
|
|
570
721
|
// stop submitting the rest of the batch too.
|
|
571
722
|
if (this.context.backendPauseRegistry?.isPaused(address)) {
|
|
572
|
-
|
|
723
|
+
pushResult({ asset, direction, dex: submittedDex, score: signalScore, leverage, marginAmount, marginSource, marginPct, success: true, reason: 'strategy_backend_paused', startedAt: signalStartedAt, endedAt: Date.now() });
|
|
573
724
|
outcome("skipped", 'strategy_backend_paused');
|
|
574
725
|
return;
|
|
575
726
|
}
|
|
@@ -654,9 +805,23 @@ export class OpenPositionAction {
|
|
|
654
805
|
// as unfilled. The code arrives either as the top-level envelope code or, when the
|
|
655
806
|
// venue reported it per-order, only inside the joined order errors — hence both.
|
|
656
807
|
const restingOrder = result.errorCode === 'CREATE_ORDER_RESTING' || /ORDER_RESTING/i.test(detail);
|
|
808
|
+
// The server aborting its own trading-engine call (UNAVAILABLE, or the unclassified
|
|
809
|
+
// INTERNAL) is not a rejection either: it says nothing about whether the venue
|
|
810
|
+
// accepted the order, and the next pass's book is too young to answer. Freeing the
|
|
811
|
+
// slot here is how one ambiguous failure becomes two positions on one slot — the
|
|
812
|
+
// same class this fix closes. Hold it on the ambiguous TTL, exactly as the thrown
|
|
813
|
+
// timeout path below does.
|
|
814
|
+
const outcomeUnknown = !restingOrder && isOutcomeUnknownCode(result.errorCode);
|
|
657
815
|
if (restingOrder) {
|
|
658
816
|
ledger.settle(pendingKey, 'resting', result.orders[0]?.orderId ?? undefined);
|
|
659
817
|
}
|
|
818
|
+
else if (outcomeUnknown) {
|
|
819
|
+
logger.error('open outcome unknown — order may be live', new Error(detail), {
|
|
820
|
+
asset,
|
|
821
|
+
errorCode: result.errorCode,
|
|
822
|
+
});
|
|
823
|
+
ledger.settle(pendingKey, 'ambiguous');
|
|
824
|
+
}
|
|
660
825
|
else {
|
|
661
826
|
// A rejected submit holds nothing — free the slot and the margin immediately.
|
|
662
827
|
ledger.release(pendingKey);
|
|
@@ -665,6 +830,9 @@ export class OpenPositionAction {
|
|
|
665
830
|
if (remainingMargin != null)
|
|
666
831
|
remainingMargin += marginAmount;
|
|
667
832
|
}
|
|
833
|
+
// Same reason code the thrown timeout uses: downstream (and the agent reading it)
|
|
834
|
+
// must reconcile against real position state, not treat a live order as rejected.
|
|
835
|
+
const failureReason = outcomeUnknown ? 'timeout' : 'position_open_failed';
|
|
668
836
|
// Backend says the strategy is PAUSED/CLOSED: arm the pause registry so
|
|
669
837
|
// subsequent signals skip instead of re-submitting (2026-07 storm class),
|
|
670
838
|
// and tell the user once. Classify with the structured envelope fields
|
|
@@ -688,22 +856,22 @@ export class OpenPositionAction {
|
|
|
688
856
|
emitEvent(logger, () => buildRuntimePausedEvent('backend_paused', 'Backend paused', detail, address));
|
|
689
857
|
}
|
|
690
858
|
}
|
|
691
|
-
|
|
859
|
+
pushResult({
|
|
692
860
|
asset, direction, dex: submittedDex, score: signalScore,
|
|
693
861
|
leverage, marginAmount, marginSource, marginPct, orderType,
|
|
694
|
-
success: false, reason:
|
|
862
|
+
success: false, reason: failureReason, error: detail,
|
|
695
863
|
startedAt: signalStartedAt, endedAt: Date.now(),
|
|
696
864
|
});
|
|
697
|
-
// Task 8a: the venue rejected the open submit.
|
|
865
|
+
// Task 8a: the venue rejected the open submit (or left it unresolved).
|
|
698
866
|
emitOrderFailed(this.context.hookSystem, event, address, {
|
|
699
867
|
asset,
|
|
700
868
|
direction,
|
|
701
|
-
reason:
|
|
869
|
+
reason: failureReason,
|
|
702
870
|
error: detail,
|
|
703
871
|
reduceOnly: false,
|
|
704
872
|
});
|
|
705
873
|
failedCount++;
|
|
706
|
-
outcome("failed",
|
|
874
|
+
outcome("failed", failureReason);
|
|
707
875
|
return;
|
|
708
876
|
}
|
|
709
877
|
// A successful submit proves the strategy is active again — re-arm immediately.
|
|
@@ -750,7 +918,7 @@ export class OpenPositionAction {
|
|
|
750
918
|
},
|
|
751
919
|
timestamp: new Date().toISOString(),
|
|
752
920
|
});
|
|
753
|
-
|
|
921
|
+
pushResult({
|
|
754
922
|
asset, direction, dex: submittedDex, score: signalScore,
|
|
755
923
|
leverage, marginAmount, marginSource, marginPct, orderType,
|
|
756
924
|
orderId: submittedOrder?.orderId ?? null,
|
|
@@ -796,7 +964,7 @@ export class OpenPositionAction {
|
|
|
796
964
|
// reservation stands on the ambiguous TTL so the next signal counts it rather than
|
|
797
965
|
// opening a second position on the same margin.
|
|
798
966
|
ledger.settle(pendingKey, 'ambiguous');
|
|
799
|
-
|
|
967
|
+
pushResult({
|
|
800
968
|
asset, direction, dex: submittedDex, score: signalScore,
|
|
801
969
|
leverage, marginAmount, marginSource, marginPct, orderType,
|
|
802
970
|
success: false, reason, error: message,
|