@senpi/runtime 3.0.0-dev.openclaw-upgrade-mai.20260623210452 → 3.0.0-dev.runtime-phase-2.20260623210537

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.
Files changed (196) hide show
  1. package/README.md +9 -10
  2. package/dist/actions/decision-engine.d.ts.map +1 -1
  3. package/dist/actions/decision-engine.js +0 -3
  4. package/dist/actions/decision-engine.js.map +1 -1
  5. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  6. package/dist/actions/open-position/open-position.action.js +2 -6
  7. package/dist/actions/open-position/open-position.action.js.map +1 -1
  8. package/dist/cli/guide-commands.d.ts.map +1 -1
  9. package/dist/cli/guide-commands.js +56 -43
  10. package/dist/cli/guide-commands.js.map +1 -1
  11. package/dist/cli/senpi-commands.d.ts.map +1 -1
  12. package/dist/cli/senpi-commands.js +108 -0
  13. package/dist/cli/senpi-commands.js.map +1 -1
  14. package/dist/constants/dsl/index.d.ts +0 -2
  15. package/dist/constants/dsl/index.d.ts.map +1 -1
  16. package/dist/constants/dsl/index.js +0 -2
  17. package/dist/constants/dsl/index.js.map +1 -1
  18. package/dist/dsl/engine-client/config-mapping.d.ts +0 -7
  19. package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
  20. package/dist/dsl/engine-client/config-mapping.js +2 -16
  21. package/dist/dsl/engine-client/config-mapping.js.map +1 -1
  22. package/dist/dsl/events/handlers.d.ts +2 -18
  23. package/dist/dsl/events/handlers.d.ts.map +1 -1
  24. package/dist/dsl/events/handlers.js +213 -302
  25. package/dist/dsl/events/handlers.js.map +1 -1
  26. package/dist/dsl/monitor/create-monitor.d.ts +0 -15
  27. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  28. package/dist/dsl/monitor/create-monitor.js +9 -44
  29. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  30. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  31. package/dist/dsl/monitor/process-one-position.js +1 -24
  32. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  33. package/dist/dsl/plugin/index.d.ts +1 -16
  34. package/dist/dsl/plugin/index.d.ts.map +1 -1
  35. package/dist/dsl/plugin/index.js +4 -35
  36. package/dist/dsl/plugin/index.js.map +1 -1
  37. package/dist/dsl/plugin/options.d.ts +0 -18
  38. package/dist/dsl/plugin/options.d.ts.map +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +0 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/runtime/create-scanner-module.d.ts +3 -4
  43. package/dist/runtime/create-scanner-module.d.ts.map +1 -1
  44. package/dist/runtime/create-scanner-module.js +5 -14
  45. package/dist/runtime/create-scanner-module.js.map +1 -1
  46. package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
  47. package/dist/runtime/create-scanner-providers-for-run.js +1 -12
  48. package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
  49. package/dist/runtime/load-runtime-config.js +1 -1
  50. package/dist/runtime/load-runtime-config.js.map +1 -1
  51. package/dist/runtime/map-strategies-registration.js +8 -7
  52. package/dist/runtime/map-strategies-registration.js.map +1 -1
  53. package/dist/runtime/map-strategies.d.ts.map +1 -1
  54. package/dist/runtime/map-strategies.js +3 -12
  55. package/dist/runtime/map-strategies.js.map +1 -1
  56. package/dist/runtime/reconcile-state.d.ts +0 -44
  57. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  58. package/dist/runtime/reconcile-state.js +44 -80
  59. package/dist/runtime/reconcile-state.js.map +1 -1
  60. package/dist/runtime/risk-guard.d.ts.map +1 -1
  61. package/dist/runtime/risk-guard.js +12 -17
  62. package/dist/runtime/risk-guard.js.map +1 -1
  63. package/dist/runtime/runtime-schema.d.ts +995 -2855
  64. package/dist/runtime/runtime-schema.d.ts.map +1 -1
  65. package/dist/runtime/runtime-schema.js +40 -133
  66. package/dist/runtime/runtime-schema.js.map +1 -1
  67. package/dist/runtime/runtime.d.ts +0 -26
  68. package/dist/runtime/runtime.d.ts.map +1 -1
  69. package/dist/runtime/runtime.js +5 -233
  70. package/dist/runtime/runtime.js.map +1 -1
  71. package/dist/runtime/scanner-registry.d.ts +1 -1
  72. package/dist/runtime/scanner-registry.d.ts.map +1 -1
  73. package/dist/runtime/scanner-registry.js +12 -3
  74. package/dist/runtime/scanner-registry.js.map +1 -1
  75. package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
  76. package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
  77. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  78. package/dist/runtime/supervisor/orphan-sweep.js +1 -1
  79. package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
  80. package/dist/runtime/supervisor/supervisor-utils.d.ts +1 -1
  81. package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
  82. package/dist/runtime/supervisor/supervisor-utils.js +1 -1
  83. package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
  84. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  85. package/dist/runtime-api/intake/intake-mount.js +36 -28
  86. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  87. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  88. package/dist/runtime-api/intake/process-signal-post.js +0 -0
  89. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  90. package/dist/runtime-api/intake/scanner-launcher.d.ts +4 -18
  91. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  92. package/dist/runtime-api/intake/scanner-launcher.js +3 -24
  93. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  94. package/dist/runtime-api/intake/types.d.ts +0 -6
  95. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  96. package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
  97. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  98. package/dist/runtime-api/routes/signals.schema.js +0 -5
  99. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  100. package/dist/scanners/external/liquidation-watchdog/README.md +107 -0
  101. package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +48 -0
  102. package/dist/scanners/external/liquidation-watchdog/producer.mjs +336 -0
  103. package/dist/scanners/external/momentum/README.md +148 -0
  104. package/dist/scanners/external/momentum/momentum.yaml +116 -0
  105. package/dist/scanners/external/momentum/producer.mjs +251 -0
  106. package/dist/scanners/external/scaffold/__main__.py +24 -65
  107. package/dist/scanners/external/scaffold/delivery.py +22 -56
  108. package/dist/scanners/external/scaffold/entrypoint_loader.py +48 -42
  109. package/dist/scanners/external/scaffold/journal.py +2 -13
  110. package/dist/scanners/external/scaffold/scaffold.py +128 -159
  111. package/dist/scanners/external/scaffold/signal_envelope.py +34 -177
  112. package/dist/scanners/external/shared/ingest.mjs +112 -0
  113. package/dist/scanners/external-scanner-receiver.d.ts +0 -6
  114. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  115. package/dist/scanners/external-scanner-receiver.js +1 -14
  116. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  117. package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
  118. package/dist/scripts/run-dsl-standalone-live.js +3 -5
  119. package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
  120. package/dist/senpi/client.d.ts +0 -12
  121. package/dist/senpi/client.d.ts.map +1 -1
  122. package/dist/senpi/client.js +2 -31
  123. package/dist/senpi/client.js.map +1 -1
  124. package/dist/state/state-manager.d.ts +0 -17
  125. package/dist/state/state-manager.d.ts.map +1 -1
  126. package/dist/state/state-manager.js +0 -28
  127. package/dist/state/state-manager.js.map +1 -1
  128. package/dist/strategy/strategy-state.d.ts.map +1 -1
  129. package/dist/strategy/strategy-state.js +1 -7
  130. package/dist/strategy/strategy-state.js.map +1 -1
  131. package/dist/tracking/constants.d.ts.map +1 -1
  132. package/dist/tracking/constants.js +0 -2
  133. package/dist/tracking/constants.js.map +1 -1
  134. package/dist/types/dsl/index.d.ts +1 -1
  135. package/dist/types/dsl/index.d.ts.map +1 -1
  136. package/dist/types/scanner.d.ts +0 -7
  137. package/dist/types/scanner.d.ts.map +1 -1
  138. package/examples/README.md +89 -0
  139. package/examples/scanners-consumer-quickstart.mjs +134 -0
  140. package/examples/scanners-emerging-movers-v4.mjs +287 -0
  141. package/examples/scanners-liquidation-watchdog.mjs +341 -0
  142. package/examples/scanners-live-mcp.mjs +208 -0
  143. package/examples/scanners-multi-strategy.mjs +165 -0
  144. package/examples/scanners-position-tracker-e2e.mjs +225 -0
  145. package/examples/scanners-scheduled-events.mjs +96 -0
  146. package/examples/scanners-senpi-provider-client.mjs +182 -0
  147. package/examples/scanners-single-strategy.mjs +198 -0
  148. package/examples/strategies/README.md +12 -17
  149. package/examples/strategies/dsl-showcase.yaml +68 -0
  150. package/examples/strategies/dsl.yaml +51 -0
  151. package/examples/strategies/fox-minimal-loosened.yaml +240 -0
  152. package/examples/strategies/fox-minimal.yaml +242 -0
  153. package/examples/strategies/minimal.yaml +46 -0
  154. package/examples/strategies/momentum/how-it-works.md +48 -0
  155. package/examples/strategies/momentum/momentum-guarded.yaml +161 -0
  156. package/examples/strategies/risk-showcase.yaml +93 -0
  157. package/examples/strategies/viper.yaml +67 -0
  158. package/examples/strategies/wolf.yaml +72 -0
  159. package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +123 -0
  160. package/package.json +1 -1
  161. package/dist/runtime/reconcile-decide.d.ts +0 -73
  162. package/dist/runtime/reconcile-decide.d.ts.map +0 -1
  163. package/dist/runtime/reconcile-decide.js +0 -166
  164. package/dist/runtime/reconcile-decide.js.map +0 -1
  165. package/dist/runtime/reconcile-run.d.ts +0 -62
  166. package/dist/runtime/reconcile-run.d.ts.map +0 -1
  167. package/dist/runtime/reconcile-run.js +0 -173
  168. package/dist/runtime/reconcile-run.js.map +0 -1
  169. package/dist/scanners/external/scaffold/ctx.py +0 -78
  170. package/dist/scanners/external/scaffold/requirements.txt +0 -11
  171. package/dist/scanners/external/scaffold/senpi_mcp.py +0 -250
  172. package/dist/scanners/external/scaffold/state_manager.py +0 -175
  173. package/dist/utils/keyed-mutex.d.ts +0 -37
  174. package/dist/utils/keyed-mutex.d.ts.map +0 -1
  175. package/dist/utils/keyed-mutex.js +0 -77
  176. package/dist/utils/keyed-mutex.js.map +0 -1
  177. package/examples/strategies/iguana/README.md +0 -135
  178. package/examples/strategies/iguana/recipe.yaml +0 -137
  179. package/examples/strategies/iguana/scanners/scan.py +0 -243
  180. package/examples/strategies/iguana/scanners/scoring.py +0 -152
  181. package/examples/strategies/raptor/README.md +0 -156
  182. package/examples/strategies/raptor/recipe.yaml +0 -154
  183. package/examples/strategies/raptor/scanners/scan.py +0 -349
  184. package/examples/strategies/raptor/scanners/scoring.py +0 -452
  185. package/examples/strategies/spider/README.md +0 -97
  186. package/examples/strategies/spider/scalp/recipe.yaml +0 -129
  187. package/examples/strategies/spider/scalp/scanners/scan.py +0 -219
  188. package/examples/strategies/spider/scalp/scanners/scoring.py +0 -179
  189. package/examples/strategies/spider/swing/recipe.yaml +0 -141
  190. package/examples/strategies/spider/swing/scanners/scan.py +0 -334
  191. package/examples/strategies/spider/swing/scanners/scoring.py +0 -184
  192. package/examples/strategies/turbine/README.md +0 -167
  193. package/examples/strategies/turbine/recipe-runners.yaml +0 -123
  194. package/examples/strategies/turbine/recipe-volume.yaml +0 -129
  195. package/examples/strategies/turbine/scanners/scan.py +0 -283
  196. package/examples/strategies/turbine/scanners/scoring.py +0 -190
@@ -51,7 +51,7 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
51
51
  normalizeDslCoinDex(event.coin, event.dex ?? "").coin &&
52
52
  s.direction === newDirection);
53
53
  }
54
- async function handlePositionOpened(payload, triggerReason, opts) {
54
+ async function handlePositionOpened(payload, triggerReason) {
55
55
  let event = null;
56
56
  if (isHookEvent(payload)) {
57
57
  event = hookToPositionOpenedEvent(payload);
@@ -72,125 +72,105 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
72
72
  return;
73
73
  event = normalizePositionOpenedEvent(event);
74
74
  const dexKey = event.dex ?? "";
75
- const posKey = stateKey(event.address, event.coin, dexKey);
76
- // Serialize concurrent opens on the same (address,coin,dex) key.
77
- // NON-REENTRANT: callers must NOT hold posKey when calling this function.
78
- await stateManager.withKeyLock(posKey, async () => {
79
- // Re-read inside the lock so a racing adopt that already wrote is visible.
80
- const existing = await stateManager.getDslState(event.address, event.coin, dexKey);
81
- if (existing?.active) {
82
- logger.warn("[dsl] position.opened ignored: DSL state already active for this asset/dex", {
83
- address: event.address,
84
- asset: event.coin,
85
- dex: dexKey,
86
- });
87
- return;
88
- }
89
- if (await isDuplicatePositionEvent(event)) {
90
- logger.warn("[dsl] duplicate position event ignored (same entry, size, coin, direction)", {
91
- address: event.address,
92
- asset: event.coin,
93
- });
94
- return;
95
- }
96
- const presetName = event.preset ?? config.preset;
97
- const preset = config.presets[presetName];
98
- if (!preset) {
99
- logger.warn("preset not found, skipping DSL", { presetName, asset: event.coin });
100
- return;
101
- }
102
- const entryPrice = parseFloat(String(event.entryPx));
103
- const direction = parseFloat(String(event.szi)) > 0 ? "LONG" : "SHORT";
104
- const leverage = event.leverage ?? 1;
105
- const phase1Floor = phase1FloorFromPreset(entryPrice, direction, preset.maxLossPct, leverage);
106
- const resolvedTrigger = triggerReason ?? "position_opened";
107
- const state = buildInitialState(event, preset, phase1Floor, resolvedTrigger);
108
- const floor = state.floorPrice;
109
- logger.info("[dsl] position.opened handling", {
75
+ const existing = await stateManager.getDslState(event.address, event.coin, dexKey);
76
+ if (existing?.active) {
77
+ logger.warn("[dsl] position.opened ignored: DSL state already active for this asset/dex", {
110
78
  address: event.address,
111
79
  asset: event.coin,
112
- dex: event.dex ?? "",
113
- preset: presetName,
114
- floor,
80
+ dex: dexKey,
81
+ });
82
+ return;
83
+ }
84
+ if (await isDuplicatePositionEvent(event)) {
85
+ logger.warn("[dsl] duplicate position event ignored (same entry, size, coin, direction)", {
86
+ address: event.address,
87
+ asset: event.coin,
88
+ });
89
+ return;
90
+ }
91
+ const presetName = event.preset ?? config.preset;
92
+ const preset = config.presets[presetName];
93
+ if (!preset) {
94
+ logger.warn("preset not found, skipping DSL", { presetName, asset: event.coin });
95
+ return;
96
+ }
97
+ const entryPrice = parseFloat(String(event.entryPx));
98
+ const direction = parseFloat(String(event.szi)) > 0 ? "LONG" : "SHORT";
99
+ const leverage = event.leverage ?? 1;
100
+ const phase1Floor = phase1FloorFromPreset(entryPrice, direction, preset.maxLossPct, leverage);
101
+ const resolvedTrigger = triggerReason ?? "position_opened";
102
+ const state = buildInitialState(event, preset, phase1Floor, resolvedTrigger);
103
+ const floor = state.floorPrice;
104
+ logger.info("[dsl] position.opened handling", {
105
+ address: event.address,
106
+ asset: event.coin,
107
+ dex: event.dex ?? "",
108
+ preset: presetName,
109
+ floor,
110
+ wallet: event.wallet,
111
+ });
112
+ const strategy = strategyStates.get(event.address);
113
+ if (!strategy) {
114
+ logger.warn("[dsl] no strategy found for address, skipping initial SL", { address: event.address });
115
+ state.slOrderId = null;
116
+ state.lastSlSyncError = "no strategy state for address";
117
+ state.lastError = state.lastSlSyncError;
118
+ state.lastErrorAt = now();
119
+ await stateManager.setDslState(event.address, event.coin, event.dex ?? "", state);
120
+ bus.emit(DSL_EVENTS.CREATED, buildDslCreatedPayload(event, presetName, preset, state, now()));
121
+ return;
122
+ }
123
+ const posKey = stateKey(event.address, event.coin, event.dex ?? "");
124
+ slPlacementInProgress.add(posKey);
125
+ try {
126
+ await stateManager.setDslState(event.address, event.coin, event.dex ?? "", state);
127
+ logger.info("[dsl] initial state written, calling editPosition for initial SL", {
128
+ asset: event.coin,
115
129
  wallet: event.wallet,
130
+ dex: event.dex || undefined,
131
+ floor,
116
132
  });
117
- const strategy = strategyStates.get(event.address);
118
- if (!strategy) {
119
- logger.warn("[dsl] no strategy found for address, skipping initial SL", { address: event.address });
120
- state.slOrderId = null;
121
- state.lastSlSyncError = "no strategy state for address";
122
- state.lastError = state.lastSlSyncError;
123
- state.lastErrorAt = now();
124
- await stateManager.setDslState(event.address, event.coin, event.dex ?? "", state);
125
- bus.emit(DSL_EVENTS.CREATED, buildDslCreatedPayload(event, presetName, preset, state, now()));
126
- return;
127
- }
128
- const shouldPlaceSl = opts?.placeSl !== false;
129
- if (!shouldPlaceSl) {
130
- // Adoption with deferred SL: monitor places it on the next tick via shouldSyncExchangeSl.
131
- state.slOrderId = null;
132
- state.lastSlSyncError = "adopt: SL deferred to monitor";
133
- logger.info("[dsl] initial state written, SL deferred to monitor (adopt)", {
134
- asset: event.coin,
135
- wallet: event.wallet,
136
- dex: event.dex || undefined,
137
- floor,
138
- });
139
- await stateManager.setDslState(event.address, event.coin, event.dex ?? "", state);
140
- bus.emit(DSL_EVENTS.CREATED, buildDslCreatedPayload(event, presetName, preset, state, now()));
141
- return;
142
- }
143
- slPlacementInProgress.add(posKey);
144
133
  try {
145
- await stateManager.setDslState(event.address, event.coin, event.dex ?? "", state);
146
- logger.info("[dsl] initial state written, calling editPosition for initial SL", {
147
- asset: event.coin,
148
- wallet: event.wallet,
134
+ const res = await withRetry(() => strategy.editPosition({
135
+ coin: event.coin,
149
136
  dex: event.dex || undefined,
150
- floor,
137
+ stopLoss: { triggerPx: floor },
138
+ }), {
139
+ retries: DSL_EXTERNAL_RETRIES,
140
+ delayMs: DSL_EXTERNAL_RETRY_DELAY_MS,
141
+ label: "editPosition (initial SL)",
142
+ onRetry: (attempt, remaining, err) => logger.warn("[dsl] editPosition retry", { attempt, remaining, asset: event.coin, err }),
151
143
  });
152
- try {
153
- const res = await withRetry(() => strategy.editPosition({
154
- coin: event.coin,
155
- dex: event.dex || undefined,
156
- stopLoss: { triggerPx: floor },
157
- }), {
158
- retries: DSL_EXTERNAL_RETRIES,
159
- delayMs: DSL_EXTERNAL_RETRY_DELAY_MS,
160
- label: "editPosition (initial SL)",
161
- onRetry: (attempt, remaining, err) => logger.warn("[dsl] editPosition retry", { attempt, remaining, asset: event.coin, err }),
162
- });
163
- state.slOrderId = res.slOrderId ?? null;
164
- state.lastSyncedFloorPrice = floor;
165
- state.slOrderIdUpdatedAt = now();
166
- state.lastSlSyncError = null;
167
- logger.info("[dsl] exchange SL created (position.opened)", {
168
- asset: event.coin,
169
- address: event.address,
170
- slOrderId: state.slOrderId,
171
- floorPrice: floor,
172
- entryPrice: state.entryPrice,
173
- });
174
- }
175
- catch (err) {
176
- state.slOrderId = null;
177
- state.lastSlSyncError = String(err);
178
- state.lastError = String(err);
179
- state.lastErrorAt = now();
180
- logger.warn("initial SL place failed", { asset: event.coin, err });
181
- }
182
- logger.info("[dsl] writing state after initial SL", {
144
+ state.slOrderId = res.slOrderId ?? null;
145
+ state.lastSyncedFloorPrice = floor;
146
+ state.slOrderIdUpdatedAt = now();
147
+ state.lastSlSyncError = null;
148
+ logger.info("[dsl] exchange SL created (position.opened)", {
183
149
  asset: event.coin,
150
+ address: event.address,
184
151
  slOrderId: state.slOrderId,
185
- key: posKey,
152
+ floorPrice: floor,
153
+ entryPrice: state.entryPrice,
186
154
  });
187
- await stateManager.setDslState(event.address, event.coin, event.dex ?? "", state);
188
- bus.emit(DSL_EVENTS.CREATED, buildDslCreatedPayload(event, presetName, preset, state, now()));
189
155
  }
190
- finally {
191
- slPlacementInProgress.delete(posKey);
156
+ catch (err) {
157
+ state.slOrderId = null;
158
+ state.lastSlSyncError = String(err);
159
+ state.lastError = String(err);
160
+ state.lastErrorAt = now();
161
+ logger.warn("initial SL place failed", { asset: event.coin, err });
192
162
  }
193
- });
163
+ logger.info("[dsl] writing state after initial SL", {
164
+ asset: event.coin,
165
+ slOrderId: state.slOrderId,
166
+ key: posKey,
167
+ });
168
+ await stateManager.setDslState(event.address, event.coin, event.dex ?? "", state);
169
+ bus.emit(DSL_EVENTS.CREATED, buildDslCreatedPayload(event, presetName, preset, state, now()));
170
+ }
171
+ finally {
172
+ slPlacementInProgress.delete(posKey);
173
+ }
194
174
  }
195
175
  async function handlePositionClosed(payload) {
196
176
  let event = null;
@@ -208,65 +188,62 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
208
188
  asset: event.asset,
209
189
  dex: event.dex ?? "",
210
190
  });
211
- const closedKey = stateKey(event.address, event.asset, event.dex ?? "");
212
- await stateManager.withKeyLock(closedKey, async () => {
213
- const state = await stateManager.getDslStateByAsset(event.address, event.asset, event.dex);
214
- if (!state?.active)
215
- return;
216
- logger.info("[dsl] closing active state", { asset: state.asset, slOrderId: state.slOrderId });
217
- let reason = CLOSE_REASON.MANUAL_CLOSE;
218
- if (state.slOrderId != null) {
219
- try {
220
- const strategy = strategyStates.get(state.address);
221
- const status = strategy
222
- ? await withRetry(() => strategy.getOrderStatus(state.slOrderId), {
223
- retries: DSL_EXTERNAL_RETRIES,
224
- delayMs: DSL_EXTERNAL_RETRY_DELAY_MS,
225
- label: "getOrderStatus (position.closed)",
226
- onRetry: (attempt, remaining, err) => logger.warn("[dsl] getOrderStatus retry", { attempt, remaining, asset: event.asset, err }),
227
- })
228
- : null;
229
- if (status?.filled)
230
- reason = CLOSE_REASON.EXCHANGE_SL_HIT;
231
- }
232
- catch (e) {
233
- logger.warn("getOrderStatus failed, assuming manual_close", { asset: event.asset, err: e });
234
- }
191
+ const state = await stateManager.getDslStateByAsset(event.address, event.asset, event.dex);
192
+ if (!state?.active)
193
+ return;
194
+ logger.info("[dsl] closing active state", { asset: state.asset, slOrderId: state.slOrderId });
195
+ let reason = CLOSE_REASON.MANUAL_CLOSE;
196
+ if (state.slOrderId != null) {
197
+ try {
198
+ const strategy = strategyStates.get(state.address);
199
+ const status = strategy
200
+ ? await withRetry(() => strategy.getOrderStatus(state.slOrderId), {
201
+ retries: DSL_EXTERNAL_RETRIES,
202
+ delayMs: DSL_EXTERNAL_RETRY_DELAY_MS,
203
+ label: "getOrderStatus (position.closed)",
204
+ onRetry: (attempt, remaining, err) => logger.warn("[dsl] getOrderStatus retry", { attempt, remaining, asset: event.asset, err }),
205
+ })
206
+ : null;
207
+ if (status?.filled)
208
+ reason = CLOSE_REASON.EXCHANGE_SL_HIT;
235
209
  }
236
- const rawCloseReason = fromHook ? payload.data?.closeReason : undefined;
237
- const dataCloseReason = typeof rawCloseReason === "string" ? rawCloseReason : null;
238
- const archiveCloseReason = reason === CLOSE_REASON.MANUAL_CLOSE && dataCloseReason ? dataCloseReason : reason;
239
- logger.info("[dsl] archiving position (position.closed)", {
240
- address: event.address,
241
- asset: state.asset,
242
- closeReason: archiveCloseReason,
243
- slOrderId: state.slOrderId,
244
- fromHook: fromHook,
245
- });
246
- const closedState = { ...state, active: false, closedAt: now(), closeReason: archiveCloseReason };
247
- await stateManager.archiveAndDeleteDslState(state.address, state.asset, state.dex, closedState);
248
- if (state.handoffStatus === "ok" && state.backendDslPositionId != null) {
249
- fireAndForgetBackendDelete(state, "position.closed");
210
+ catch (e) {
211
+ logger.warn("getOrderStatus failed, assuming manual_close", { asset: event.asset, err: e });
250
212
  }
251
- const closedTs = now();
252
- bus.emit(EVENTS.POSITION_CLOSED, {
253
- address: state.address,
254
- asset: state.asset,
255
- wallet: state.wallet,
256
- reason,
257
- closeReason: archiveCloseReason,
258
- timestamp: closedTs,
259
- // Provenance marker (data only, no consumer branches on it): this emit re-broadcasts a close
260
- // after archiving DSL state; the recording subscriber skips it to avoid double-counting a
261
- // close already recorded from the original (enveloped or flat) emit.
262
- source: "dsl_archive",
263
- });
264
- bus.emit(DSL_EVENTS.CLOSED, buildDslClosedEventFromState(closedState, {
265
- reason: archiveCloseReason,
266
- closeReason: archiveCloseReason,
267
- timestamp: closedTs,
268
- }));
213
+ }
214
+ const rawCloseReason = fromHook ? payload.data?.closeReason : undefined;
215
+ const dataCloseReason = typeof rawCloseReason === "string" ? rawCloseReason : null;
216
+ const archiveCloseReason = reason === CLOSE_REASON.MANUAL_CLOSE && dataCloseReason ? dataCloseReason : reason;
217
+ logger.info("[dsl] archiving position (position.closed)", {
218
+ address: event.address,
219
+ asset: state.asset,
220
+ closeReason: archiveCloseReason,
221
+ slOrderId: state.slOrderId,
222
+ fromHook: fromHook,
223
+ });
224
+ const closedState = { ...state, active: false, closedAt: now(), closeReason: archiveCloseReason };
225
+ await stateManager.archiveAndDeleteDslState(state.address, state.asset, state.dex, closedState);
226
+ if (state.handoffStatus === "ok" && state.backendDslPositionId != null) {
227
+ fireAndForgetBackendDelete(state, "position.closed");
228
+ }
229
+ const closedTs = now();
230
+ bus.emit(EVENTS.POSITION_CLOSED, {
231
+ address: state.address,
232
+ asset: state.asset,
233
+ wallet: state.wallet,
234
+ reason,
235
+ closeReason: archiveCloseReason,
236
+ timestamp: closedTs,
237
+ // Provenance marker (data only, no consumer branches on it): this emit re-broadcasts a close
238
+ // after archiving DSL state; the recording subscriber skips it to avoid double-counting a
239
+ // close already recorded from the original (enveloped or flat) emit.
240
+ source: "dsl_archive",
269
241
  });
242
+ bus.emit(DSL_EVENTS.CLOSED, buildDslClosedEventFromState(closedState, {
243
+ reason: archiveCloseReason,
244
+ closeReason: archiveCloseReason,
245
+ timestamp: closedTs,
246
+ }));
270
247
  }
271
248
  async function handlePositionFlipped(payload) {
272
249
  let event = null;
@@ -283,36 +260,26 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
283
260
  archiveAsset: event.asset,
284
261
  newCoin: event.coin,
285
262
  });
286
- // Archive the OLD state under its own key — lock released before calling handlePositionOpened
287
- // (which locks the NEW key itself; keys differ after a flip).
288
263
  const oldState = await stateManager.getDslStateByAsset(event.address, event.asset, event.dex);
289
264
  if (oldState?.active) {
290
- const oldKey = stateKey(event.address, oldState.asset, oldState.dex ?? "");
291
- await stateManager.withKeyLock(oldKey, async () => {
292
- // Re-read to confirm still active (concurrent close could have already archived it).
293
- const currentOld = await stateManager.getDslStateByAsset(event.address, event.asset, event.dex);
294
- if (!currentOld?.active)
295
- return;
296
- logger.info("[dsl] archiving position (position.flipped)", {
297
- address: event.address,
298
- asset: currentOld.asset,
299
- dex: currentOld.dex ?? "",
300
- closeReason: CLOSE_REASON.FLIPPED,
301
- slOrderId: currentOld.slOrderId,
302
- });
303
- const closedState = { ...currentOld, active: false, closedAt: now(), closeReason: CLOSE_REASON.FLIPPED };
304
- // Must use canonical keys from loaded state — not event.asset (e.g. xyz:BASE vs BASE + dex xyz
305
- // produce different stateKey paths; wrong asset would archive nothing and leave the live file active).
306
- await stateManager.archiveAndDeleteDslState(event.address, currentOld.asset, currentOld.dex ?? "", closedState);
307
- if (currentOld.handoffStatus === "ok" && currentOld.backendDslPositionId != null) {
308
- fireAndForgetBackendDelete(currentOld, "position.flipped");
309
- }
265
+ logger.info("[dsl] archiving position (position.flipped)", {
266
+ address: event.address,
267
+ asset: oldState.asset,
268
+ dex: oldState.dex ?? "",
269
+ closeReason: CLOSE_REASON.FLIPPED,
270
+ slOrderId: oldState.slOrderId,
310
271
  });
272
+ const closedState = { ...oldState, active: false, closedAt: now(), closeReason: CLOSE_REASON.FLIPPED };
273
+ // Must use canonical keys from loaded state — not event.asset (e.g. xyz:BASE vs BASE + dex xyz
274
+ // produce different stateKey paths; wrong asset would archive nothing and leave the live file active).
275
+ await stateManager.archiveAndDeleteDslState(event.address, oldState.asset, oldState.dex ?? "", closedState);
276
+ if (oldState.handoffStatus === "ok" && oldState.backendDslPositionId != null) {
277
+ fireAndForgetBackendDelete(oldState, "position.flipped");
278
+ }
311
279
  }
312
280
  else {
313
281
  logger.warn("[dsl] position.flipped: no active DSL state matched for archive (check asset/dex vs state file). If a live file still exists for this market, the next position.opened may be ignored as duplicate.", { address: event.address, asset: event.asset, dex: event.dex ?? "" });
314
282
  }
315
- // Old-key lock is released above. handlePositionOpened locks the NEW key itself.
316
283
  // New row uses `coin` (and dex) from the flip payload — not `asset`, which identifies the pre-flip state for archival.
317
284
  const openedPayload = {
318
285
  address: event.address,
@@ -338,13 +305,8 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
338
305
  if (!raw)
339
306
  return;
340
307
  const event = normalizePositionOpenedEvent(raw);
341
- // Archive the OLD state under its key — lock released BEFORE calling handlePositionOpened
342
- // (which re-locks the same key; holding it across the nested call would deadlock).
343
- const archiveKey = stateKey(event.address, event.coin, event.dex ?? "");
344
- await stateManager.withKeyLock(archiveKey, async () => {
345
- const oldState = await stateManager.getDslStateByAsset(event.address, event.coin, event.dex);
346
- if (!oldState?.active)
347
- return;
308
+ const oldState = await stateManager.getDslStateByAsset(event.address, event.coin, event.dex);
309
+ if (oldState?.active) {
348
310
  const closedState = {
349
311
  ...oldState,
350
312
  active: false,
@@ -357,8 +319,7 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
357
319
  if (oldState.handoffStatus === "ok" && oldState.backendDslPositionId != null) {
358
320
  fireAndForgetBackendDelete(oldState, "position.increased");
359
321
  }
360
- });
361
- // archiveKey lock is released; handlePositionOpened re-acquires it safely.
322
+ }
362
323
  await handlePositionOpened(event, "position_increased");
363
324
  }
364
325
  async function handlePositionDecreased(payload) {
@@ -372,13 +333,8 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
372
333
  if (!raw)
373
334
  return;
374
335
  const event = normalizePositionOpenedEvent(raw);
375
- // Archive the OLD state under its key — lock released BEFORE calling handlePositionOpened
376
- // (which re-locks the same key; holding it across the nested call would deadlock).
377
- const archiveKey = stateKey(event.address, event.coin, event.dex ?? "");
378
- await stateManager.withKeyLock(archiveKey, async () => {
379
- const oldState = await stateManager.getDslStateByAsset(event.address, event.coin, event.dex);
380
- if (!oldState?.active)
381
- return;
336
+ const oldState = await stateManager.getDslStateByAsset(event.address, event.coin, event.dex);
337
+ if (oldState?.active) {
382
338
  const closedState = {
383
339
  ...oldState,
384
340
  active: false,
@@ -391,8 +347,7 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
391
347
  if (oldState.handoffStatus === "ok" && oldState.backendDslPositionId != null) {
392
348
  fireAndForgetBackendDelete(oldState, "position.decreased");
393
349
  }
394
- });
395
- // archiveKey lock is released; handlePositionOpened re-acquires it safely.
350
+ }
396
351
  await handlePositionOpened(event, "position_decreased");
397
352
  }
398
353
  async function deleteDslsForStrategy(address) {
@@ -400,70 +355,62 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
400
355
  const ts = now();
401
356
  let abandoned = 0;
402
357
  for (const key of keys) {
403
- // Peek outside the lock to skip inactive entries cheaply.
404
- const peeked = await stateManager.getOrLoad(key);
405
- if (!peeked?.active)
358
+ const state = await stateManager.getOrLoad(key);
359
+ if (!state?.active)
406
360
  continue;
407
- // Lock the per-position key for the archive RMW.
408
- const perPosKey = stateKey(peeked.address, peeked.asset, peeked.dex ?? "");
409
- await stateManager.withKeyLock(perPosKey, async () => {
410
- const state = await stateManager.getOrLoad(key);
411
- if (!state?.active)
412
- return;
413
- abandoned += 1;
414
- const strategy = strategyStates.get(state.address);
415
- const cleanupPromises = [];
416
- if (strategy) {
417
- cleanupPromises.push(withRetry(() => strategy.editPosition({ coin: state.asset, dex: state.dex || undefined }), {
418
- retries: DSL_EXTERNAL_RETRIES,
419
- delayMs: DSL_EXTERNAL_RETRY_DELAY_MS,
420
- label: "editPosition (cancel SL, dsl delete)",
421
- onRetry: (attempt, remaining, err) => logger.warn("[dsl] editPosition cancel SL retry", { attempt, remaining, err }),
422
- }).then(() => undefined).catch((err) => {
423
- logger.warn("[dsl] cancel SL failed during deleteDslsForStrategy; continuing archive", {
424
- address,
425
- asset: state.asset,
426
- err,
427
- });
428
- }));
429
- }
430
- else {
431
- logger.warn("[dsl] no strategy found for address, skipping SL cancel", { address });
432
- }
433
- if (state.handoffStatus === "ok" && state.backendDslPositionId != null && strategy) {
434
- cleanupPromises.push(strategy.deleteRatchetStop({
361
+ abandoned += 1;
362
+ const strategy = strategyStates.get(state.address);
363
+ const cleanupPromises = [];
364
+ if (strategy) {
365
+ cleanupPromises.push(withRetry(() => strategy.editPosition({ coin: state.asset, dex: state.dex || undefined }), {
366
+ retries: DSL_EXTERNAL_RETRIES,
367
+ delayMs: DSL_EXTERNAL_RETRY_DELAY_MS,
368
+ label: "editPosition (cancel SL, dsl delete)",
369
+ onRetry: (attempt, remaining, err) => logger.warn("[dsl] editPosition cancel SL retry", { attempt, remaining, err }),
370
+ }).then(() => undefined).catch((err) => {
371
+ logger.warn("[dsl] cancel SL failed during deleteDslsForStrategy; continuing archive", {
372
+ address,
435
373
  asset: state.asset,
436
- dex: state.dex || undefined,
437
- }).then(() => {
438
- logger.info("[dsl] deleted backend dsl position (deleteDslsForStrategy)", {
439
- address,
440
- asset: state.asset,
441
- });
442
- }).catch((err) => {
443
- logger.warn("[dsl] failed to delete backend dsl position (deleteDslsForStrategy)", {
444
- address,
445
- asset: state.asset,
446
- err,
447
- });
448
- }));
449
- }
450
- await Promise.all(cleanupPromises);
451
- const closedState = {
452
- ...state,
453
- active: false,
454
- closedAt: ts,
455
- closeReason: CLOSE_REASON.DSL_DELETED,
456
- };
457
- await stateManager.archiveAndDeleteDslState(state.address, state.asset, state.dex, closedState);
458
- bus.emit(DSL_EVENTS.DELETED, {
459
- address: state.address,
374
+ err,
375
+ });
376
+ }));
377
+ }
378
+ else {
379
+ logger.warn("[dsl] no strategy found for address, skipping SL cancel", { address });
380
+ }
381
+ if (state.handoffStatus === "ok" && state.backendDslPositionId != null && strategy) {
382
+ cleanupPromises.push(strategy.deleteRatchetStop({
460
383
  asset: state.asset,
461
- dex: state.dex ?? "",
462
- triggerReason: state.triggerReason,
463
- __positionId: state.positionId ?? state.correlationId,
464
- __correlationId: state.correlationId,
465
- timestamp: ts,
466
- });
384
+ dex: state.dex || undefined,
385
+ }).then(() => {
386
+ logger.info("[dsl] deleted backend dsl position (deleteDslsForStrategy)", {
387
+ address,
388
+ asset: state.asset,
389
+ });
390
+ }).catch((err) => {
391
+ logger.warn("[dsl] failed to delete backend dsl position (deleteDslsForStrategy)", {
392
+ address,
393
+ asset: state.asset,
394
+ err,
395
+ });
396
+ }));
397
+ }
398
+ await Promise.all(cleanupPromises);
399
+ const closedState = {
400
+ ...state,
401
+ active: false,
402
+ closedAt: ts,
403
+ closeReason: CLOSE_REASON.DSL_DELETED,
404
+ };
405
+ await stateManager.archiveAndDeleteDslState(state.address, state.asset, state.dex, closedState);
406
+ bus.emit(DSL_EVENTS.DELETED, {
407
+ address: state.address,
408
+ asset: state.asset,
409
+ dex: state.dex ?? "",
410
+ triggerReason: state.triggerReason,
411
+ __positionId: state.positionId ?? state.correlationId,
412
+ __correlationId: state.correlationId,
413
+ timestamp: ts,
467
414
  });
468
415
  }
469
416
  return abandoned;
@@ -471,40 +418,6 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
471
418
  function isSlPlacementInProgress(key) {
472
419
  return slPlacementInProgress.has(key);
473
420
  }
474
- /**
475
- * Adopt an orphaned exchange position into DSL.
476
- * Calls handlePositionOpened directly — NO bus emit of on_position_opened / position.opened.
477
- * triggerReason is always "adopted" so formatDslCreated returns null (no user "Opened" message).
478
- */
479
- async function adoptPosition(payload, opts) {
480
- await handlePositionOpened(payload, "adopted", opts);
481
- }
482
- /**
483
- * Resize: archive any existing ACTIVE DSL state for the (coin, dex), then call adoptPosition.
484
- * Archive uses CLOSE_REASON.RECONCILE_RESIZE. Lock is released before the nested adopt
485
- * (same pattern as handlePositionIncreased) to avoid deadlock.
486
- */
487
- async function resizePosition(payload, opts) {
488
- const dexKey = payload.dex ?? "";
489
- const archiveKey = stateKey(payload.address, payload.coin, dexKey);
490
- await stateManager.withKeyLock(archiveKey, async () => {
491
- const oldState = await stateManager.getDslStateByAsset(payload.address, payload.coin, dexKey);
492
- if (!oldState?.active)
493
- return;
494
- const closedState = {
495
- ...oldState,
496
- active: false,
497
- closedAt: now(),
498
- closeReason: CLOSE_REASON.RECONCILE_RESIZE,
499
- };
500
- await stateManager.archiveAndDeleteDslState(payload.address, oldState.asset, oldState.dex, closedState);
501
- if (oldState.handoffStatus === "ok" && oldState.backendDslPositionId != null) {
502
- fireAndForgetBackendDelete(oldState, "reconcile_resize");
503
- }
504
- });
505
- // archiveKey lock released above; adoptPosition (via handlePositionOpened) re-acquires it safely.
506
- await adoptPosition(payload, opts);
507
- }
508
421
  function fireAndForgetBackendDelete(state, trigger) {
509
422
  const resolveAndDelete = async () => {
510
423
  const strategy = strategyStates.get(state.address);
@@ -552,8 +465,6 @@ export function createEventHandlers(config, stateManager, strategyStates, bus, _
552
465
  handlePositionDecreased,
553
466
  deleteDslsForStrategy,
554
467
  isSlPlacementInProgress,
555
- adoptPosition,
556
- resizePosition,
557
468
  };
558
469
  }
559
470
  //# sourceMappingURL=handlers.js.map