@senpi/runtime 1.1.0-dev.runtime-phase-2.20260612060625 → 1.1.0-dev.runtime-phase-2.20260617180011
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.map +1 -1
- package/dist/actions/close-position/close-position.action.js +55 -13
- package/dist/actions/close-position/close-position.action.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +66 -15
- 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 +10 -1
- package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
- package/dist/actions/tracking-helpers.d.ts +16 -1
- package/dist/actions/tracking-helpers.d.ts.map +1 -1
- package/dist/actions/tracking-helpers.js +55 -9
- package/dist/actions/tracking-helpers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +30 -0
- package/dist/dsl/monitor/create-monitor.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +114 -0
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/recording/emitter.d.ts +7 -1
- package/dist/recording/emitter.d.ts.map +1 -1
- package/dist/recording/index.d.ts +1 -1
- package/dist/recording/index.d.ts.map +1 -1
- package/dist/recording/index.js.map +1 -1
- package/dist/recording/noop-emitter.d.ts +6 -0
- package/dist/recording/noop-emitter.d.ts.map +1 -1
- package/dist/recording/noop-emitter.js +6 -0
- package/dist/recording/noop-emitter.js.map +1 -1
- package/dist/recording/otel/constants.d.ts +45 -3
- package/dist/recording/otel/constants.d.ts.map +1 -1
- package/dist/recording/otel/constants.js +79 -3
- package/dist/recording/otel/constants.js.map +1 -1
- package/dist/recording/otel/ids.d.ts +11 -0
- package/dist/recording/otel/ids.d.ts.map +1 -1
- package/dist/recording/otel/ids.js +26 -1
- package/dist/recording/otel/ids.js.map +1 -1
- package/dist/recording/otel/index.d.ts +1 -1
- package/dist/recording/otel/index.d.ts.map +1 -1
- package/dist/recording/otel/index.js +1 -1
- package/dist/recording/otel/index.js.map +1 -1
- package/dist/recording/otel/mappers.d.ts +60 -2
- package/dist/recording/otel/mappers.d.ts.map +1 -1
- package/dist/recording/otel/mappers.js +384 -43
- package/dist/recording/otel/mappers.js.map +1 -1
- package/dist/recording/otel/otel-emitter.d.ts +7 -1
- package/dist/recording/otel/otel-emitter.d.ts.map +1 -1
- package/dist/recording/otel/otel-emitter.js +26 -2
- package/dist/recording/otel/otel-emitter.js.map +1 -1
- package/dist/recording/otel/redaction.d.ts +22 -6
- package/dist/recording/otel/redaction.d.ts.map +1 -1
- package/dist/recording/otel/redaction.js +54 -6
- package/dist/recording/otel/redaction.js.map +1 -1
- package/dist/recording/records.d.ts +227 -3
- package/dist/recording/records.d.ts.map +1 -1
- package/dist/recording/wiring/record-builders.d.ts +59 -26
- package/dist/recording/wiring/record-builders.d.ts.map +1 -1
- package/dist/recording/wiring/record-builders.js +309 -61
- package/dist/recording/wiring/record-builders.js.map +1 -1
- package/dist/recording/wiring/subscriber.d.ts.map +1 -1
- package/dist/recording/wiring/subscriber.js +59 -57
- package/dist/recording/wiring/subscriber.js.map +1 -1
- package/dist/runtime/load-runtime-config.d.ts.map +1 -1
- package/dist/runtime/load-runtime-config.js +2 -3
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +7 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/scanners/implementations/momentum.d.ts.map +1 -1
- package/dist/scanners/implementations/momentum.js +16 -0
- package/dist/scanners/implementations/momentum.js.map +1 -1
- package/dist/tracking/position-id-registry.d.ts +9 -0
- package/dist/tracking/position-id-registry.d.ts.map +1 -1
- package/dist/tracking/position-id-registry.js +10 -4
- package/dist/tracking/position-id-registry.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,19 +7,20 @@
|
|
|
7
7
|
* Timestamps: the bus carries ISO strings (and, for a scan result, an epoch-ms number); every record
|
|
8
8
|
* stores epoch ms, converted once here.
|
|
9
9
|
*/
|
|
10
|
-
import type { DslTransitionKind, DslTransitionRecord, PositionLifecycleEvent, PositionLifecycleRecord, RuntimeErrorRecord, RuntimeLifecycleRecord, RuntimeStateRecord, ScannerRunRecord,
|
|
10
|
+
import type { DecisionMadeRecord, DslCloseAttemptRecord, DslTickRecord, DslTickUnit, DslTransitionKind, DslTransitionRecord, OrderEventKind, OrderEventRecord, PositionLifecycleEvent, PositionLifecycleRecord, RuntimeErrorRecord, RuntimeLifecycleRecord, RuntimeStateRecord, ScannerRunRecord, ScannerSummaryRecord, SignalEvaluatedRecord, SignalOutcomeRecord } from "../records.js";
|
|
11
11
|
/** From a `tracking.scanner_run` / `tracking.scanner_run_skipped` / `tracking.scanner_run_error`
|
|
12
12
|
* event (complete runs carry the whole `ScanResult`; skip/error carry status + reason). */
|
|
13
13
|
export declare function buildScannerRun(payload: unknown, nowMs: number): ScannerRunRecord | null;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The `scanner.summary` anti-spam log record, derived from the SAME `tracking.scanner_run` payload
|
|
16
|
+
* that {@link buildScannerRun} reads (its `data.summary.funnel.dropped`) — no new bus event.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` when there is no `data.summary.funnel`: skip/error runs and scanners without a
|
|
19
|
+
* funnel produce no summary event (the survivors are detailed by their own `signal.outcome` events,
|
|
20
|
+
* and "didn't run at all" is `scanner_run.skipped`). One record per scan; the dropped candidates are
|
|
21
|
+
* one Body array, never one row per drop.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildScannerSummary(payload: unknown, nowMs: number): ScannerSummaryRecord | null;
|
|
23
24
|
/**
|
|
24
25
|
* Read-only position.id lookup, (address, dex, asset) → the live id or undefined. Injected by
|
|
25
26
|
* the runtime as a narrow accessor over the PositionIdRegistry — the registry is the single
|
|
@@ -27,27 +28,48 @@ export declare function buildPendingDecision(payload: unknown, nowMs: number): P
|
|
|
27
28
|
*/
|
|
28
29
|
export type PositionIdResolver = (address: string, dex: string, asset: string) => string | undefined;
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Build the whole pass record from a `tracking.action_result` payload alone — the producers echo
|
|
32
|
+
* the decision window and scan provenance onto the payload (`scannerId`, `scannerSignals`,
|
|
33
|
+
* `minConfidence`, `decisionStartedAt`, `decisionEndedAt`, `runId`), and the decision itself rides
|
|
34
|
+
* `result.data._decisionMeta`, so no pairing with `tracking.decision` is needed.
|
|
35
|
+
*
|
|
36
|
+
* `evaluations` is one entry per per-signal result, each enriched with the matching input signal
|
|
37
|
+
* (or a summary synthesized from the result entry when none matches — e.g. close-position's
|
|
38
|
+
* static-params path, which carries no `scannerSignals`). A pass without `_decisionMeta` yields no
|
|
39
|
+
* `decision` (the contract allows a decision-less pass).
|
|
40
|
+
*
|
|
41
|
+
* The pass span runs `decisionStartedAt` → action_result-ts; when the start stamp is absent
|
|
42
|
+
* (defensive only — the bus is in-process) `startedAt = endedAt`. On clock skew
|
|
43
|
+
* (`endedAt < startedAt`) the start is pulled up to `endedAt`, yielding a zero (never negative)
|
|
44
|
+
* duration.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildSignalEvaluated(actionResultPayload: unknown, nowMs: number, resolvePositionId?: PositionIdResolver): SignalEvaluatedRecord | null;
|
|
47
|
+
/**
|
|
48
|
+
* The pass's `decision.made` log record, derived from the built pass record — `null` when the pass
|
|
49
|
+
* carried no decision (a decision-less pass yields no judgment event). One per pass.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildDecisionMade(record: SignalEvaluatedRecord): DecisionMadeRecord | null;
|
|
52
|
+
/**
|
|
53
|
+
* One `signal.outcome` log record per evaluation in the pass, derived from the built pass record.
|
|
54
|
+
* `rawSignal` is the evaluation's {@link SignalSummary} (incl. its `meta`/`factors`) — the variable
|
|
55
|
+
* scanner evidence; redaction over it happens in the mapper, not here.
|
|
34
56
|
*/
|
|
35
|
-
export declare function
|
|
57
|
+
export declare function buildSignalOutcomes(record: SignalEvaluatedRecord): SignalOutcomeRecord[];
|
|
36
58
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `decision` (the contract allows a decision-less pass); zero duration (`startedAt = endedAt` = the
|
|
41
|
-
* action_result timestamp).
|
|
59
|
+
* From the existing `tracking.dsl_close_attempt` monitor event — the second consumer (alongside the
|
|
60
|
+
* tick trace). Fields at the payload root: `positionId`, `asset`, `dex`, `closeReason`, `attempt`,
|
|
61
|
+
* `outcome` ("ok"|"error"), `error`, ISO `timestamp`. `null` when the payload is not an object.
|
|
42
62
|
*/
|
|
43
|
-
export declare function
|
|
63
|
+
export declare function buildDslCloseAttempt(payload: unknown, nowMs: number): DslCloseAttemptRecord | null;
|
|
44
64
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
65
|
+
* From the additive `tracking.order_*` events the open/close actions fire (the HookEvent shape:
|
|
66
|
+
* fields under a `data` envelope). One record per event; `eventKind` discriminates which fields the
|
|
67
|
+
* payload carries (placed: order/size; filled: fill; failed: reason/error). `positionId` maps from
|
|
68
|
+
* `__positionId`. `null` when no `asset` is resolvable — the asset is the one always-present scalar.
|
|
69
|
+
*
|
|
70
|
+
* Faithful population only (no redaction here — the mapper is the single redaction seam).
|
|
49
71
|
*/
|
|
50
|
-
export declare function
|
|
72
|
+
export declare function buildOrderEvent(eventKind: OrderEventKind, payload: unknown, nowMs: number): OrderEventRecord | null;
|
|
51
73
|
/**
|
|
52
74
|
* From an `on_position_*` event. Action-driven events arrive as HookEvents (fields under a `data`
|
|
53
75
|
* envelope); the DSL monitor / reconcile paths emit `on_position_closed` flat (fields at the root,
|
|
@@ -59,6 +81,17 @@ export declare function buildDecisionOnlyEvaluated(pending: PendingDecision): Si
|
|
|
59
81
|
export declare function buildPositionLifecycle(event: PositionLifecycleEvent, payload: unknown, nowMs: number): PositionLifecycleRecord | null;
|
|
60
82
|
/** From a `dsl.*` event (fields live at the payload root, with an ISO `timestamp`). */
|
|
61
83
|
export declare function buildDslTransition(transition: DslTransitionKind, payload: unknown, nowMs: number): DslTransitionRecord | null;
|
|
84
|
+
/**
|
|
85
|
+
* From the monitor's `tracking.dsl_tick` / `tracking.dsl_position` / `tracking.dsl_close_attempt`
|
|
86
|
+
* / `tracking.dsl_sl_update` events (fields at the payload root). Each event yields one record —
|
|
87
|
+
* the deterministic ids derived from `tickId` (and `tickId:positionKey`, both bootId-scoped by
|
|
88
|
+
* the mapper) thread them into one trace, so no pairing is needed here. Essentials are `tickId`
|
|
89
|
+
* plus a numeric work window; on clock skew (`endedAt < startedAt`) the start is pulled up to the
|
|
90
|
+
* end, matching the pass-window convention. The price-fetch window is kept only when both stamps
|
|
91
|
+
* are present (a half window is fabricated extent). No `nowMs` fallback: work windows are
|
|
92
|
+
* producer-stamped; nothing here is fabricated.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildDslTick(unit: DslTickUnit, payload: unknown): DslTickRecord | null;
|
|
62
95
|
/** From `signal_processing_error` — a bucket-2 error with no specific domain record home. */
|
|
63
96
|
export declare function buildRuntimeError(payload: unknown, nowMs: number): RuntimeErrorRecord | null;
|
|
64
97
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-builders.d.ts","sourceRoot":"","sources":["../../../src/recording/wiring/record-builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"record-builders.d.ts","sourceRoot":"","sources":["../../../src/recording/wiring/record-builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAEV,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAElB,gBAAgB,EAChB,oBAAoB,EAEpB,qBAAqB,EAIrB,mBAAmB,EAIpB,MAAM,eAAe,CAAC;AAoDvB;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA6BxF;AAID;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CA6ChG;AAsED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;AA0DrG;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,mBAAmB,EAAE,OAAO,EAC5B,KAAK,EAAE,MAAM,EACb,iBAAiB,CAAC,EAAE,kBAAkB,GACrC,qBAAqB,GAAG,IAAI,CAkD9B;AA8BD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,qBAAqB,GAAG,kBAAkB,GAAG,IAAI,CAiB1F;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,mBAAmB,EAAE,CA0BxF;AASD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAclG;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA2BnH;AAUD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAuCrI;AAID,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAgD7H;AASD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAkCtF;AAID,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CA2B5F;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAczJ;AAED,uCAAuC;AACvC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAWlG;AAID,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAcrH"}
|
|
@@ -71,6 +71,7 @@ export function buildScannerRun(payload, nowMs) {
|
|
|
71
71
|
return {
|
|
72
72
|
type: "scanner_run",
|
|
73
73
|
scannerId: str(data.scannerId) ?? "",
|
|
74
|
+
runId: str(data.runId),
|
|
74
75
|
outcome,
|
|
75
76
|
statusDetail: status,
|
|
76
77
|
startedAt,
|
|
@@ -84,33 +85,63 @@ export function buildScannerRun(payload, nowMs) {
|
|
|
84
85
|
error: outcome === "error" ? str(data.reason) : undefined,
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// ─── Scanner funnel summary (Task 8a, derived from `tracking.scanner_run`) ────────
|
|
89
|
+
/**
|
|
90
|
+
* The `scanner.summary` anti-spam log record, derived from the SAME `tracking.scanner_run` payload
|
|
91
|
+
* that {@link buildScannerRun} reads (its `data.summary.funnel.dropped`) — no new bus event.
|
|
92
|
+
*
|
|
93
|
+
* Returns `null` when there is no `data.summary.funnel`: skip/error runs and scanners without a
|
|
94
|
+
* funnel produce no summary event (the survivors are detailed by their own `signal.outcome` events,
|
|
95
|
+
* and "didn't run at all" is `scanner_run.skipped`). One record per scan; the dropped candidates are
|
|
96
|
+
* one Body array, never one row per drop.
|
|
97
|
+
*/
|
|
98
|
+
export function buildScannerSummary(payload, nowMs) {
|
|
89
99
|
const p = asObject(payload);
|
|
90
100
|
const data = asObject(p?.data);
|
|
91
101
|
if (!data)
|
|
92
102
|
return null;
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
confidence: num(d.confidence) ?? 0,
|
|
102
|
-
confidenceThreshold: num(data.minConfidence),
|
|
103
|
-
reasoning: str(d.reasoning) ?? "",
|
|
104
|
-
model: str(data.decisionModel) ?? str(tel?.model),
|
|
105
|
-
tokensUsed: numOrNull(tel?.tokensUsed),
|
|
106
|
-
durationMs: num(tel?.durationMs),
|
|
107
|
-
payload: asObject(d.payload),
|
|
108
|
-
};
|
|
109
|
-
const signals = Array.isArray(data.scannerSignals)
|
|
110
|
-
? data.scannerSignals.map((s) => buildSignalSummary(s, startedAtMs))
|
|
103
|
+
const summary = asObject(data.summary);
|
|
104
|
+
const funnel = asObject(summary?.funnel);
|
|
105
|
+
if (!funnel)
|
|
106
|
+
return null;
|
|
107
|
+
const droppedCandidates = Array.isArray(funnel.dropped)
|
|
108
|
+
? funnel.dropped
|
|
109
|
+
.map((d) => asObject(d))
|
|
110
|
+
.filter((d) => d !== undefined)
|
|
111
111
|
: [];
|
|
112
|
-
|
|
112
|
+
// Survivors: the emitted signals' assets — a small linkage list to the per-signal events.
|
|
113
|
+
const passedAssets = Array.isArray(data.signals)
|
|
114
|
+
? data.signals
|
|
115
|
+
.map((s) => str(asObject(s)?.asset))
|
|
116
|
+
.filter((a) => a !== undefined)
|
|
117
|
+
: [];
|
|
118
|
+
const droppedByReason = {};
|
|
119
|
+
for (const d of droppedCandidates) {
|
|
120
|
+
const reason = str(d.reason);
|
|
121
|
+
if (reason === undefined)
|
|
122
|
+
continue;
|
|
123
|
+
droppedByReason[reason] = (droppedByReason[reason] ?? 0) + 1;
|
|
124
|
+
}
|
|
125
|
+
const droppedCount = droppedCandidates.length;
|
|
126
|
+
const passedCount = num(data.signalCount) ?? passedAssets.length;
|
|
127
|
+
// Grain is "one per scan that *considered* candidates": an idle scan (nothing passed, nothing
|
|
128
|
+
// dropped) carries an empty funnel and yields no summary — emitting an all-empty row every tick
|
|
129
|
+
// would defeat the anti-spam intent and risk evicting high-value rows on transport overflow.
|
|
130
|
+
if (droppedCount === 0 && passedCount === 0)
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
type: "scanner_summary",
|
|
134
|
+
occurredAt: toEpochMs(data.timestamp, toEpochMs(p?.timestamp, nowMs)),
|
|
135
|
+
scannerId: str(data.scannerId) ?? "",
|
|
136
|
+
scannedCount: num(data.scannedCount) ?? passedCount + droppedCount,
|
|
137
|
+
passedCount,
|
|
138
|
+
droppedCount,
|
|
139
|
+
droppedByReason: Object.keys(droppedByReason).length > 0 ? droppedByReason : undefined,
|
|
140
|
+
passedAssets: passedAssets.length > 0 ? passedAssets : undefined,
|
|
141
|
+
droppedCandidates: droppedCount > 0 ? droppedCandidates : undefined,
|
|
142
|
+
};
|
|
113
143
|
}
|
|
144
|
+
// ─── Signal evaluation (single-sourced from `tracking.action_result`) ────────────
|
|
114
145
|
/** Reason codes where the system evaluated a trade and actively blocked it (see {@link classifyOutcome}). */
|
|
115
146
|
const REJECTED_REASONS = new Set(["insufficient_margin", "invalid_direction", "no_margin_configured", "below_min_notional"]);
|
|
116
147
|
/** Reason codes where no trade was applicable / attempted. */
|
|
@@ -164,13 +195,13 @@ function buildOutcome(raw, positionId) {
|
|
|
164
195
|
availableMarginAmount: num(r.availableMarginAmount),
|
|
165
196
|
};
|
|
166
197
|
}
|
|
167
|
-
/** The per-signal `results[]` entries from a `tracking.action_result` payload. */
|
|
198
|
+
/** The `data` envelope, embedded `result`, and per-signal `results[]` entries from a `tracking.action_result` payload. */
|
|
168
199
|
function actionResults(actionResultPayload) {
|
|
169
200
|
const p = asObject(actionResultPayload);
|
|
170
201
|
const data = asObject(p?.data);
|
|
171
202
|
const result = asObject(data?.result);
|
|
172
203
|
const resultData = asObject(result?.data);
|
|
173
|
-
return { data, results: Array.isArray(resultData?.results) ? resultData.results : [] };
|
|
204
|
+
return { data, result, resultData, results: Array.isArray(resultData?.results) ? resultData.results : [] };
|
|
174
205
|
}
|
|
175
206
|
/**
|
|
176
207
|
* The V2 stamping rule: every per-signal outcome that pertains to a real position — open,
|
|
@@ -188,6 +219,11 @@ function resolveOutcomePositionId(resolve, address, r, asset) {
|
|
|
188
219
|
/**
|
|
189
220
|
* One {@link SignalEvaluation} per per-signal result entry, each enriched with the matching input
|
|
190
221
|
* signal (via `findSignal`) or a synthesized summary from the result entry itself when none matches.
|
|
222
|
+
*
|
|
223
|
+
* The per-signal work window (`startedAt`/`endedAt`, stamped by the action loop) is threaded onto
|
|
224
|
+
* the evaluation. A half window is fabricated extent, so both stamps must be present or neither is
|
|
225
|
+
* kept; on clock skew (`endedAt < startedAt`) the start is pulled up to the end, matching the
|
|
226
|
+
* pass-window convention.
|
|
191
227
|
*/
|
|
192
228
|
function buildEvaluations(results, findSignal, fallbackTs, resolvePositionId, address) {
|
|
193
229
|
return results.map((raw) => {
|
|
@@ -200,67 +236,223 @@ function buildEvaluations(results, findSignal, fallbackTs, resolvePositionId, ad
|
|
|
200
236
|
score: num(r.score) ?? 0,
|
|
201
237
|
timestamp: fallbackTs,
|
|
202
238
|
};
|
|
203
|
-
|
|
239
|
+
let startedAt = num(r.startedAt);
|
|
240
|
+
const endedAt = num(r.endedAt);
|
|
241
|
+
if (startedAt !== undefined && endedAt !== undefined && endedAt < startedAt)
|
|
242
|
+
startedAt = endedAt;
|
|
243
|
+
return {
|
|
244
|
+
signal,
|
|
245
|
+
outcome: buildOutcome(r, resolveOutcomePositionId(resolvePositionId, address, r, asset)),
|
|
246
|
+
...(startedAt !== undefined && endedAt !== undefined ? { startedAt, endedAt } : {}),
|
|
247
|
+
};
|
|
204
248
|
});
|
|
205
249
|
}
|
|
206
250
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
251
|
+
* Build the whole pass record from a `tracking.action_result` payload alone — the producers echo
|
|
252
|
+
* the decision window and scan provenance onto the payload (`scannerId`, `scannerSignals`,
|
|
253
|
+
* `minConfidence`, `decisionStartedAt`, `decisionEndedAt`, `runId`), and the decision itself rides
|
|
254
|
+
* `result.data._decisionMeta`, so no pairing with `tracking.decision` is needed.
|
|
255
|
+
*
|
|
256
|
+
* `evaluations` is one entry per per-signal result, each enriched with the matching input signal
|
|
257
|
+
* (or a summary synthesized from the result entry when none matches — e.g. close-position's
|
|
258
|
+
* static-params path, which carries no `scannerSignals`). A pass without `_decisionMeta` yields no
|
|
259
|
+
* `decision` (the contract allows a decision-less pass).
|
|
260
|
+
*
|
|
261
|
+
* The pass span runs `decisionStartedAt` → action_result-ts; when the start stamp is absent
|
|
262
|
+
* (defensive only — the bus is in-process) `startedAt = endedAt`. On clock skew
|
|
263
|
+
* (`endedAt < startedAt`) the start is pulled up to `endedAt`, yielding a zero (never negative)
|
|
264
|
+
* duration.
|
|
211
265
|
*/
|
|
212
|
-
export function buildSignalEvaluated(
|
|
266
|
+
export function buildSignalEvaluated(actionResultPayload, nowMs, resolvePositionId) {
|
|
267
|
+
const { data, result, resultData, results } = actionResults(actionResultPayload);
|
|
268
|
+
if (!data)
|
|
269
|
+
return null;
|
|
213
270
|
const p = asObject(actionResultPayload);
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
let startedAt = pending.startedAtMs;
|
|
217
|
-
let endedAt = toEpochMs(p?.timestamp, nowMs);
|
|
271
|
+
const endedAt = toEpochMs(p?.timestamp, nowMs);
|
|
272
|
+
let startedAt = num(data.decisionStartedAt) ?? endedAt;
|
|
218
273
|
if (endedAt < startedAt)
|
|
219
274
|
startedAt = endedAt;
|
|
275
|
+
const signals = Array.isArray(data.scannerSignals)
|
|
276
|
+
? data.scannerSignals.map((s) => buildSignalSummary(s, startedAt))
|
|
277
|
+
: [];
|
|
278
|
+
// Indexed once (first occurrence wins, matching find() semantics) — this runs in the producer's
|
|
279
|
+
// synchronous emit path, so the per-evaluation lookup must not rescan the signal array.
|
|
280
|
+
const byAsset = new Map();
|
|
281
|
+
for (const s of signals)
|
|
282
|
+
if (!byAsset.has(s.asset))
|
|
283
|
+
byAsset.set(s.asset, s);
|
|
284
|
+
const evaluations = buildEvaluations(results, (asset) => byAsset.get(asset), startedAt, resolvePositionId, str(p?.address));
|
|
285
|
+
const meta = asObject(resultData?._decisionMeta);
|
|
286
|
+
const decision = meta
|
|
287
|
+
? {
|
|
288
|
+
execute: bool(meta.execute) ?? false,
|
|
289
|
+
actionType: str(result?.action_type) ?? "",
|
|
290
|
+
actionName: str(data.actionName),
|
|
291
|
+
mode: str(meta.mode),
|
|
292
|
+
confidence: num(meta.confidence) ?? 0,
|
|
293
|
+
confidenceThreshold: num(data.minConfidence),
|
|
294
|
+
reasoning: str(meta.reasoning) ?? "",
|
|
295
|
+
model: str(meta.model),
|
|
296
|
+
tokensUsed: numOrNull(meta.tokensUsed),
|
|
297
|
+
durationMs: num(meta.durationMs),
|
|
298
|
+
endedAt: num(data.decisionEndedAt),
|
|
299
|
+
}
|
|
300
|
+
: undefined;
|
|
220
301
|
return {
|
|
221
302
|
type: "signal_evaluated",
|
|
222
|
-
scannerId:
|
|
303
|
+
scannerId: str(data.scannerId),
|
|
304
|
+
runId: str(data.runId),
|
|
223
305
|
startedAt,
|
|
224
306
|
endedAt,
|
|
225
|
-
decision
|
|
307
|
+
decision,
|
|
226
308
|
evaluations,
|
|
227
309
|
};
|
|
228
310
|
}
|
|
311
|
+
// ─── Self-learning event log: decision.made + signal.outcome (Task 8) ─────────────
|
|
312
|
+
//
|
|
313
|
+
// Both are DERIVED from the SAME built SignalEvaluatedRecord (single-source, DRY) — no re-parse of
|
|
314
|
+
// the payload, no new bus event. The mapper does the redaction; these builders populate faithfully.
|
|
315
|
+
/** Decision action type → the coarse open/close/none intent. */
|
|
316
|
+
function actionIntent(actionType) {
|
|
317
|
+
if (actionType === "open_position")
|
|
318
|
+
return "open";
|
|
319
|
+
if (actionType === "close_position")
|
|
320
|
+
return "close";
|
|
321
|
+
return "none";
|
|
322
|
+
}
|
|
323
|
+
/** The four-value disposition from the granular per-signal {@link SignalOutcomeResult}. */
|
|
324
|
+
function disposition(result) {
|
|
325
|
+
switch (result) {
|
|
326
|
+
case "executed":
|
|
327
|
+
return "accepted";
|
|
328
|
+
case "below_confidence":
|
|
329
|
+
return "rejected";
|
|
330
|
+
case "error":
|
|
331
|
+
return "error";
|
|
332
|
+
// rejected (actively blocked by a gate) and skipped (not attempted) both read as "blocked"
|
|
333
|
+
// at the disposition grain; the granular `reasonCode` is kept verbatim for the why.
|
|
334
|
+
default:
|
|
335
|
+
return "blocked";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
229
338
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* The record carries the per-signal outcomes with signals synthesized from the result entries and no
|
|
233
|
-
* `decision` (the contract allows a decision-less pass); zero duration (`startedAt = endedAt` = the
|
|
234
|
-
* action_result timestamp).
|
|
339
|
+
* The pass's `decision.made` log record, derived from the built pass record — `null` when the pass
|
|
340
|
+
* carried no decision (a decision-less pass yields no judgment event). One per pass.
|
|
235
341
|
*/
|
|
236
|
-
export function
|
|
237
|
-
const
|
|
238
|
-
if (!
|
|
342
|
+
export function buildDecisionMade(record) {
|
|
343
|
+
const d = record.decision;
|
|
344
|
+
if (!d)
|
|
239
345
|
return null;
|
|
240
|
-
const p = asObject(actionResultPayload);
|
|
241
|
-
const endedAt = toEpochMs(p?.timestamp, nowMs);
|
|
242
346
|
return {
|
|
243
|
-
type: "
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
347
|
+
type: "decision_made",
|
|
348
|
+
occurredAt: record.endedAt,
|
|
349
|
+
mode: d.mode,
|
|
350
|
+
byLlm: d.mode === "llm",
|
|
351
|
+
model: d.model,
|
|
352
|
+
tokensUsed: d.tokensUsed,
|
|
353
|
+
confidence: d.confidence,
|
|
354
|
+
confidenceThreshold: d.confidenceThreshold,
|
|
355
|
+
durationMs: d.durationMs,
|
|
356
|
+
actionIntent: actionIntent(d.actionType),
|
|
357
|
+
// Market context + marginSource are NOT on the payload today → left undefined (see impl notes).
|
|
358
|
+
reasoning: d.reasoning,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* One `signal.outcome` log record per evaluation in the pass, derived from the built pass record.
|
|
363
|
+
* `rawSignal` is the evaluation's {@link SignalSummary} (incl. its `meta`/`factors`) — the variable
|
|
364
|
+
* scanner evidence; redaction over it happens in the mapper, not here.
|
|
365
|
+
*/
|
|
366
|
+
export function buildSignalOutcomes(record) {
|
|
367
|
+
return record.evaluations.map((ev) => {
|
|
368
|
+
const { signal, outcome } = ev;
|
|
369
|
+
return {
|
|
370
|
+
type: "signal_outcome",
|
|
371
|
+
occurredAt: record.endedAt,
|
|
372
|
+
scannerId: record.scannerId,
|
|
373
|
+
signalType: signal.signalType !== "" ? signal.signalType : undefined,
|
|
374
|
+
asset: signal.asset,
|
|
375
|
+
direction: signal.direction,
|
|
376
|
+
score: signal.score,
|
|
377
|
+
signalTs: signal.timestamp,
|
|
378
|
+
factors: signal.factors,
|
|
379
|
+
result: disposition(outcome.result),
|
|
380
|
+
reasonCode: outcome.reasonCode,
|
|
381
|
+
positionId: outcome.positionId,
|
|
382
|
+
marginAmount: outcome.marginAmount,
|
|
383
|
+
leverage: outcome.leverage,
|
|
384
|
+
notionalValue: outcome.notionalValue,
|
|
385
|
+
size: outcome.size,
|
|
386
|
+
// marginSource is NOT on the payload today → left undefined (see impl notes).
|
|
387
|
+
orderId: outcome.orderId,
|
|
388
|
+
errorName: outcome.errorName,
|
|
389
|
+
rawSignal: { ...signal },
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
// ─── Self-learning event log: dsl.close_attempt (Task 8) ──────────────────────────
|
|
394
|
+
/** Coerces the producer's outcome string to the close-attempt result enum; default `error`. */
|
|
395
|
+
function closeAttemptResult(value) {
|
|
396
|
+
return value === "ok" ? "ok" : "error";
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* From the existing `tracking.dsl_close_attempt` monitor event — the second consumer (alongside the
|
|
400
|
+
* tick trace). Fields at the payload root: `positionId`, `asset`, `dex`, `closeReason`, `attempt`,
|
|
401
|
+
* `outcome` ("ok"|"error"), `error`, ISO `timestamp`. `null` when the payload is not an object.
|
|
402
|
+
*/
|
|
403
|
+
export function buildDslCloseAttempt(payload, nowMs) {
|
|
404
|
+
const p = asObject(payload);
|
|
405
|
+
if (!p)
|
|
406
|
+
return null;
|
|
407
|
+
return {
|
|
408
|
+
type: "dsl_close_attempt",
|
|
409
|
+
occurredAt: toEpochMs(p.timestamp, nowMs),
|
|
410
|
+
positionId: str(p.positionId),
|
|
411
|
+
asset: str(p.asset),
|
|
412
|
+
dex: str(p.dex),
|
|
413
|
+
reason: str(p.closeReason),
|
|
414
|
+
attempt: num(p.attempt),
|
|
415
|
+
result: closeAttemptResult(p.outcome),
|
|
416
|
+
error: str(p.error),
|
|
248
417
|
};
|
|
249
418
|
}
|
|
419
|
+
// ─── Self-learning event log: order.{placed,filled,failed} (Task 8a) ──────────────
|
|
250
420
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
421
|
+
* From the additive `tracking.order_*` events the open/close actions fire (the HookEvent shape:
|
|
422
|
+
* fields under a `data` envelope). One record per event; `eventKind` discriminates which fields the
|
|
423
|
+
* payload carries (placed: order/size; filled: fill; failed: reason/error). `positionId` maps from
|
|
424
|
+
* `__positionId`. `null` when no `asset` is resolvable — the asset is the one always-present scalar.
|
|
425
|
+
*
|
|
426
|
+
* Faithful population only (no redaction here — the mapper is the single redaction seam).
|
|
255
427
|
*/
|
|
256
|
-
export function
|
|
428
|
+
export function buildOrderEvent(eventKind, payload, nowMs) {
|
|
429
|
+
const p = asObject(payload);
|
|
430
|
+
const data = asObject(p?.data);
|
|
431
|
+
if (!data)
|
|
432
|
+
return null;
|
|
433
|
+
const asset = str(data.asset);
|
|
434
|
+
if (asset === undefined)
|
|
435
|
+
return null;
|
|
257
436
|
return {
|
|
258
|
-
type: "
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
437
|
+
type: "order_event",
|
|
438
|
+
event: eventKind,
|
|
439
|
+
occurredAt: toEpochMs(p?.timestamp, nowMs),
|
|
440
|
+
positionId: str(data.__positionId),
|
|
441
|
+
orderId: str(data.orderId),
|
|
442
|
+
asset,
|
|
443
|
+
direction: direction(data.direction),
|
|
444
|
+
orderType: str(data.orderType),
|
|
445
|
+
size: num(data.size),
|
|
446
|
+
limitPrice: num(data.limitPrice),
|
|
447
|
+
reduceOnly: bool(data.reduceOnly),
|
|
448
|
+
fillPrice: num(data.fillPrice),
|
|
449
|
+
fillSize: num(data.fillSize),
|
|
450
|
+
executionAsMaker: boolOrNull(data.executionAsMaker),
|
|
451
|
+
fees: num(data.fees),
|
|
452
|
+
partial: bool(data.partial),
|
|
453
|
+
reason: str(data.reason),
|
|
454
|
+
errorName: str(data.errorName),
|
|
455
|
+
error: str(data.error),
|
|
264
456
|
};
|
|
265
457
|
}
|
|
266
458
|
// ─── Position lifecycle ──────────────────────────────────────────────────────
|
|
@@ -316,6 +508,8 @@ export function buildPositionLifecycle(event, payload, nowMs) {
|
|
|
316
508
|
marginAmount: num(data.marginAmount),
|
|
317
509
|
filled: boolOrNull(data.filled),
|
|
318
510
|
reasoning: str(data.reasoning),
|
|
511
|
+
// Stamped by the position-tracker action on `on_position_opened` only (absent elsewhere).
|
|
512
|
+
reconciliationSource: str(data.reconciliationSource),
|
|
319
513
|
};
|
|
320
514
|
}
|
|
321
515
|
// ─── DSL transitions ─────────────────────────────────────────────────────────
|
|
@@ -370,6 +564,59 @@ export function buildDslTransition(transition, payload, nowMs) {
|
|
|
370
564
|
backendDslPositionId: typeof p.backendDslPositionId === "string" ? p.backendDslPositionId : p.backendDslPositionId === null ? null : undefined,
|
|
371
565
|
};
|
|
372
566
|
}
|
|
567
|
+
// ─── DSL monitor tick work units ─────────────────────────────────────────────
|
|
568
|
+
/** Coerces the producer's outcome string to the record's closed enum; anything else is dropped. */
|
|
569
|
+
function dslTickOutcome(value) {
|
|
570
|
+
return value === "ok" || value === "error" ? value : undefined;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* From the monitor's `tracking.dsl_tick` / `tracking.dsl_position` / `tracking.dsl_close_attempt`
|
|
574
|
+
* / `tracking.dsl_sl_update` events (fields at the payload root). Each event yields one record —
|
|
575
|
+
* the deterministic ids derived from `tickId` (and `tickId:positionKey`, both bootId-scoped by
|
|
576
|
+
* the mapper) thread them into one trace, so no pairing is needed here. Essentials are `tickId`
|
|
577
|
+
* plus a numeric work window; on clock skew (`endedAt < startedAt`) the start is pulled up to the
|
|
578
|
+
* end, matching the pass-window convention. The price-fetch window is kept only when both stamps
|
|
579
|
+
* are present (a half window is fabricated extent). No `nowMs` fallback: work windows are
|
|
580
|
+
* producer-stamped; nothing here is fabricated.
|
|
581
|
+
*/
|
|
582
|
+
export function buildDslTick(unit, payload) {
|
|
583
|
+
const p = asObject(payload);
|
|
584
|
+
if (!p)
|
|
585
|
+
return null;
|
|
586
|
+
const tickId = str(p.tickId);
|
|
587
|
+
let startedAt = num(p.startedAt);
|
|
588
|
+
const endedAt = num(p.endedAt);
|
|
589
|
+
if (tickId === undefined || startedAt === undefined || endedAt === undefined)
|
|
590
|
+
return null;
|
|
591
|
+
if (endedAt < startedAt)
|
|
592
|
+
startedAt = endedAt;
|
|
593
|
+
const pricesStartedAt = num(p.pricesStartedAt);
|
|
594
|
+
const pricesEndedAt = num(p.pricesEndedAt);
|
|
595
|
+
const bothPrices = pricesStartedAt !== undefined && pricesEndedAt !== undefined;
|
|
596
|
+
return {
|
|
597
|
+
type: "dsl_tick",
|
|
598
|
+
unit,
|
|
599
|
+
tickId,
|
|
600
|
+
startedAt,
|
|
601
|
+
endedAt,
|
|
602
|
+
strategyCount: num(p.strategyCount),
|
|
603
|
+
activePositionCount: num(p.activePositionCount),
|
|
604
|
+
positionsProcessed: num(p.positionsProcessed),
|
|
605
|
+
pricesStartedAt: bothPrices ? pricesStartedAt : undefined,
|
|
606
|
+
pricesEndedAt: bothPrices ? pricesEndedAt : undefined,
|
|
607
|
+
positionKey: str(p.key),
|
|
608
|
+
positionId: str(p.positionId),
|
|
609
|
+
asset: str(p.asset),
|
|
610
|
+
dex: str(p.dex),
|
|
611
|
+
outcome: dslTickOutcome(p.outcome),
|
|
612
|
+
error: str(p.error),
|
|
613
|
+
closeReason: str(p.closeReason),
|
|
614
|
+
attempt: num(p.attempt),
|
|
615
|
+
newSLPrice: num(p.newSLPrice),
|
|
616
|
+
slOrderId: num(p.slOrderId),
|
|
617
|
+
syncReason: str(p.syncReason),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
373
620
|
// ─── Runtime error ───────────────────────────────────────────────────────────
|
|
374
621
|
/** From `signal_processing_error` — a bucket-2 error with no specific domain record home. */
|
|
375
622
|
export function buildRuntimeError(payload, nowMs) {
|
|
@@ -398,6 +645,7 @@ export function buildRuntimeError(payload, nowMs) {
|
|
|
398
645
|
message,
|
|
399
646
|
errorName,
|
|
400
647
|
scannerId: str(p.scannerId),
|
|
648
|
+
runId: str(p.runId),
|
|
401
649
|
stack,
|
|
402
650
|
};
|
|
403
651
|
}
|