@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
@@ -1,175 +0,0 @@
1
- """Disk-backed, bounded, transactional state series for the scaffold core.
2
-
3
- `StateManager` is the `ctx.state` capability the supervised external scanner
4
- author sees: a list-like series of dict records, persisted between ticks and
5
- bounded by `state_history_max_count`.
6
-
7
- Shape (per scanner-and-ctx-design.md §2.3):
8
-
9
- StateManager(state_path, state_history_max_count)
10
-
11
- Author surface:
12
- append(record) record MUST be a dict; a reserved `recorded_at` (float
13
- SECONDS, time.time() magnitude — never ms) is injected.
14
- scaffold wins on a `recorded_at` collision. returns None.
15
- RAISES when state_history_max_count is 0/unset — history
16
- disabled is a hard error, never a silent no-op.
17
- recent(n) last n entries, chronological (oldest->newest); n<=0 -> [].
18
- last() most recent entry, or None if empty.
19
- __len__ / __getitem__ (index + slice) / __iter__ (oldest -> newest).
20
-
21
- Internals: backed by collections.deque(maxlen=state_history_max_count).
22
- Loading an on-disk series longer than the bound keeps the NEWEST entries
23
- (deque eviction drops the oldest).
24
-
25
- Transactional (one commit per tick):
26
- commit() persists the working buffer via the atomic write_state.
27
- JSON-serializability is enforced LOUD here — a failed persist
28
- RAISES and does NOT advance the committed snapshot, leaving the
29
- prior on-disk state intact.
30
- rollback() discards mid-tick mutations: the working buffer is restored to
31
- the last committed snapshot.
32
- read-your-writes within a tick: appended records are visible via
33
- len/recent/last/iter/getitem BEFORE commit.
34
- """
35
-
36
- from __future__ import annotations
37
-
38
- import copy
39
- import time
40
- from collections import deque
41
-
42
- from .scaffold_state import read_state, write_state
43
-
44
- # Reserved, scaffold-owned key injected into every record. Float seconds.
45
- RECORDED_AT_KEY = "recorded_at"
46
-
47
-
48
- class StateManager:
49
- """A disk-backed, bounded, transactional series of dict records."""
50
-
51
- def __init__(self, state_path: str, state_history_max_count):
52
- self._state_path = state_path
53
- # 0 / None / falsy => history disabled. Kept as-is so append can raise.
54
- self._max_count = state_history_max_count
55
-
56
- # Load any persisted series. read_state never raises: missing/corrupt
57
- # -> None. A non-list payload is treated as "no usable history".
58
- # state_path None => no disk persistence (commit advances the in-memory
59
- # snapshot but never writes). read_state needs a real path.
60
- loaded = read_state(state_path) if state_path is not None else None
61
- if not isinstance(loaded, list):
62
- loaded = []
63
-
64
- # The committed snapshot is the last durably-persisted series; the
65
- # working buffer is what the current tick reads/mutates. Both are
66
- # bounded the same way (deque(maxlen=...)); loading a longer-than-bound
67
- # series keeps the NEWEST entries (deque evicts from the left).
68
- self._committed = self._new_deque(loaded)
69
- self._working = self._snapshot(self._committed)
70
-
71
- # -- internals ---------------------------------------------------------
72
-
73
- def _new_deque(self, initial=()):
74
- """A deque bounded by max_count. When history is disabled (max_count is
75
- 0/None) the bound is 0 — nothing can ever be buffered."""
76
- maxlen = self._max_count if self._max_count else 0
77
- return deque(initial, maxlen=maxlen)
78
-
79
- def _snapshot(self, source):
80
- """An independent bounded deque whose records share NO mutable state
81
- with `source`. Committed and working buffers must never alias the same
82
- dict objects — otherwise a mid-tick mutation of a record (an author
83
- scribbling on a dict handed back by last()/recent()/getitem) would
84
- survive a rollback and corrupt the very snapshot the rollback restores.
85
- Deep-copy at every snapshot boundary (load / commit / rollback)."""
86
- return self._new_deque(copy.deepcopy(entry) for entry in source)
87
-
88
- # -- author surface ----------------------------------------------------
89
-
90
- def append(self, record) -> None:
91
- """Append a dict record to the working buffer.
92
-
93
- Injects the reserved `recorded_at` (float seconds, scaffold wins on
94
- collision). Raises TypeError if record is not a dict. Raises if history
95
- is disabled (state_history_max_count 0/unset) — never a silent no-op.
96
- """
97
- if not self._max_count:
98
- raise ValueError(
99
- "state is disabled: state_history_max_count is 0 or unset "
100
- "(set it > 0 to use ctx.state.append)"
101
- )
102
- # bool is an int subclass but never a dict; explicit dict check rejects
103
- # lists/tuples/sets/strings/numbers/None/objects/bool alike.
104
- if not isinstance(record, dict):
105
- raise TypeError(
106
- f"state record must be a dict, got {type(record).__name__}"
107
- )
108
-
109
- # Copy so the author's dict is never mutated, then stamp recorded_at.
110
- # scaffold wins on collision: assign AFTER copying author keys.
111
- entry = dict(record)
112
- entry[RECORDED_AT_KEY] = time.time()
113
- self._working.append(entry)
114
- return None
115
-
116
- def recent(self, n) -> list:
117
- """The last n entries, chronological (oldest->newest).
118
-
119
- n <= 0 returns []; n greater than the buffer returns the whole buffer.
120
- Negative n is NOT a from-the-end slice — it is meaningless for "last n"
121
- and yields [].
122
- """
123
- if n <= 0:
124
- return []
125
- items = list(self._working)
126
- return items[-n:]
127
-
128
- def last(self):
129
- """The most recent entry, or None if empty."""
130
- if not self._working:
131
- return None
132
- return self._working[-1]
133
-
134
- # -- sequence protocol -------------------------------------------------
135
-
136
- def __len__(self) -> int:
137
- return len(self._working)
138
-
139
- def __getitem__(self, key):
140
- # deque supports integer indexing (raising IndexError out of range) but
141
- # not slicing; materialise a list so slices work with Python semantics
142
- # (clamping, steps, negative bounds) and index errors still propagate.
143
- if isinstance(key, slice):
144
- return list(self._working)[key]
145
- return self._working[key]
146
-
147
- def __iter__(self):
148
- return iter(self._working)
149
-
150
- # -- transaction -------------------------------------------------------
151
-
152
- def commit(self) -> None:
153
- """Persist the working buffer atomically via write_state.
154
-
155
- JSON-serializability is enforced LOUD: write_state returns False on a
156
- non-serializable value (or IO failure) without touching the existing
157
- file. We raise on that False so a failed persist never silently
158
- advances the committed snapshot — the prior on-disk state stays intact.
159
- """
160
- payload = list(self._working)
161
- # No disk path => commit advances the in-memory snapshot only.
162
- ok = True if self._state_path is None else write_state(self._state_path, payload)
163
- if not ok:
164
- raise RuntimeError(
165
- f"failed to persist state to {self._state_path} "
166
- "(non-serializable value or IO error); state not advanced"
167
- )
168
- # Durable: advance the committed snapshot to match what is on disk.
169
- # Deep-copy so the committed snapshot does not alias working records.
170
- self._committed = self._snapshot(self._working)
171
-
172
- def rollback(self) -> None:
173
- """Discard mid-tick mutations: restore the working buffer to the last
174
- committed snapshot. Never touches disk."""
175
- self._working = self._snapshot(self._committed)
@@ -1,37 +0,0 @@
1
- /**
2
- * KeyedMutex — per-key async serialization primitive.
3
- *
4
- * Concurrent calls with DIFFERENT keys run in parallel.
5
- * Concurrent calls with the SAME key are queued and run strictly FIFO.
6
- *
7
- * NON-REENTRANT by design: calling withKeyLock(k, ...) from *inside* a fn
8
- * that is already holding lock k will deadlock. Callers must not nest
9
- * same-key locks. If you need reentrance, restructure the call to pass the
10
- * already-computed value in or extract the critical section into a helper
11
- * that runs outside the lock.
12
- *
13
- * Memory: once a key has no holder and no waiters its internal entry is
14
- * deleted — the Map does not grow unboundedly.
15
- */
16
- export declare class KeyedMutex {
17
- private readonly locks;
18
- /**
19
- * Acquire the lock for `key`, execute `fn`, release the lock, and return
20
- * the resolved value of `fn`. If `fn` throws or rejects, the error is
21
- * propagated to the caller and the lock is released so the next waiter
22
- * can proceed.
23
- */
24
- withKeyLock<T>(key: string, fn: () => Promise<T> | T): Promise<T>;
25
- /**
26
- * Number of keys that currently have an active holder or at least one
27
- * waiter. Returns 0 once all locks have fully drained.
28
- * Exposed for testing; not part of the production surface.
29
- */
30
- size(): number;
31
- /**
32
- * Whether an entry exists for `key` (holder or waiters present).
33
- * Exposed for testing; not part of the production surface.
34
- */
35
- hasKey(key: string): boolean;
36
- }
37
- //# sourceMappingURL=keyed-mutex.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"keyed-mutex.d.ts","sourceRoot":"","sources":["../../src/utils/keyed-mutex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IAEtD;;;;;OAKG;IACG,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAwCvE;;;;OAIG;IACH,IAAI,IAAI,MAAM;IAId;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;CAG7B"}
@@ -1,77 +0,0 @@
1
- /**
2
- * KeyedMutex — per-key async serialization primitive.
3
- *
4
- * Concurrent calls with DIFFERENT keys run in parallel.
5
- * Concurrent calls with the SAME key are queued and run strictly FIFO.
6
- *
7
- * NON-REENTRANT by design: calling withKeyLock(k, ...) from *inside* a fn
8
- * that is already holding lock k will deadlock. Callers must not nest
9
- * same-key locks. If you need reentrance, restructure the call to pass the
10
- * already-computed value in or extract the critical section into a helper
11
- * that runs outside the lock.
12
- *
13
- * Memory: once a key has no holder and no waiters its internal entry is
14
- * deleted — the Map does not grow unboundedly.
15
- */
16
- export class KeyedMutex {
17
- locks = new Map();
18
- /**
19
- * Acquire the lock for `key`, execute `fn`, release the lock, and return
20
- * the resolved value of `fn`. If `fn` throws or rejects, the error is
21
- * propagated to the caller and the lock is released so the next waiter
22
- * can proceed.
23
- */
24
- async withKeyLock(key, fn) {
25
- const existing = this.locks.get(key);
26
- let releaseCurrent;
27
- // A promise that the *next* waiter will chain on.
28
- const current = new Promise((resolve) => {
29
- releaseCurrent = resolve;
30
- });
31
- if (existing == null) {
32
- // First caller for this key — create the entry.
33
- this.locks.set(key, { tail: current, refCount: 1 });
34
- // No predecessor to wait for; run immediately.
35
- }
36
- else {
37
- // There is already a holder (or a queue). Chain behind the current tail.
38
- existing.refCount += 1;
39
- const predecessor = existing.tail;
40
- // Update tail so the *next* arrival chains behind us.
41
- existing.tail = current;
42
- // Wait for the predecessor to finish.
43
- await predecessor;
44
- }
45
- try {
46
- return await fn();
47
- }
48
- finally {
49
- // Release: let the next waiter proceed.
50
- releaseCurrent();
51
- // Decrement refcount; if nobody remains, prune the entry.
52
- const entry = this.locks.get(key);
53
- if (entry != null) {
54
- entry.refCount -= 1;
55
- if (entry.refCount === 0) {
56
- this.locks.delete(key);
57
- }
58
- }
59
- }
60
- }
61
- /**
62
- * Number of keys that currently have an active holder or at least one
63
- * waiter. Returns 0 once all locks have fully drained.
64
- * Exposed for testing; not part of the production surface.
65
- */
66
- size() {
67
- return this.locks.size;
68
- }
69
- /**
70
- * Whether an entry exists for `key` (holder or waiters present).
71
- * Exposed for testing; not part of the production surface.
72
- */
73
- hasKey(key) {
74
- return this.locks.has(key);
75
- }
76
- }
77
- //# sourceMappingURL=keyed-mutex.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"keyed-mutex.js","sourceRoot":"","sources":["../../src/utils/keyed-mutex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,MAAM,OAAO,UAAU;IACJ,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IAEtD;;;;;OAKG;IACH,KAAK,CAAC,WAAW,CAAI,GAAW,EAAE,EAAwB;QACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAErC,IAAI,cAA2B,CAAC;QAChC,kDAAkD;QAClD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAC5C,cAAc,GAAG,OAAO,CAAC;QAC3B,CAAC,CAAC,CAAC;QAEH,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;YACrB,gDAAgD;YAChD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;YACpD,+CAA+C;QACjD,CAAC;aAAM,CAAC;YACN,yEAAyE;YACzE,QAAQ,CAAC,QAAQ,IAAI,CAAC,CAAC;YACvB,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC;YAClC,sDAAsD;YACtD,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC;YACxB,sCAAsC;YACtC,MAAM,WAAW,CAAC;QACpB,CAAC;QAED,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;gBAAS,CAAC;YACT,wCAAwC;YACxC,cAAc,EAAE,CAAC;YAEjB,0DAA0D;YAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBAClB,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC;gBACpB,IAAI,KAAK,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;oBACzB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,IAAI;QACF,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,GAAW;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;CACF"}
@@ -1,135 +0,0 @@
1
- # IGUANA (v2 supervised demo)
2
-
3
- Iguana is a **single-wallet, single-recipe** XYZ **macro index-trend** strategy:
4
- the closest thing to an index fund, but 24/7 on Hyperliquid. It trend-follows the
5
- two broad XYZ indices and trades the stronger move — **two assets, one decision
6
- per tick**. No stock-picking, no commodities, no pre-IPO. Onboarding tier.
7
-
8
- Ported from `senpi-skills/iguana` (producer v1.0.1, config v1.0.0) to the runtime
9
- v2 supervised external-scanner contract. The standalone producer daemon is gone;
10
- the runtime supervises `scanners/scan.py` and calls `scan()` every
11
- `interval_seconds` (300s).
12
-
13
- ## The strategy (`scanners/scan.py`)
14
-
15
- Index-trend pipeline, run once per tick:
16
-
17
- 1. **Whitelist** — the two broad XYZ indices `{xyz:SP500, xyz:XYZ100}`.
18
- 2. **Trend strength** — for each non-held index, fetch 4h candles
19
- (`market_get_asset_data`, `candle_intervals ["4h"]`) and compute the **4-day
20
- trend strength**: the % change of the latest 4h close vs the close
21
- `trendLookbackBars` (**24** = 4 days) bars ago.
22
- 3. **Pick the strongest** — the index with the highest `|strength|` above
23
- `minTrendPct` (**1.5%**).
24
- 4. **Score** — base **3**, **+2** if `|strength| ≥ strongTrendPct` (**4.0%**),
25
- **+1** if volume is rising (> **15%** over the last 6 bars). Emit only if
26
- `score ≥ minScore` (**4**).
27
- 5. **Direction** = sign of the trend (LONG if up, SHORT if down).
28
- 6. **Emit the single strongest** index trend — at most one signal per tick.
29
-
30
- The pure trend/scoring functions (`trend_strength`, `trend_direction`,
31
- `pick_strongest_trend`, `volume_trend`, `build_thesis`) were already unit-tested
32
- in the source repo and are ported **verbatim** into `scoring.py`.
33
-
34
- Sizing rides on each signal's `data{}` so the `OPEN_POSITION` rule action sizes
35
- identically to the source:
36
-
37
- - **marginUsd** = `account_value × marginPct`, `marginPct` **0.20**. Dual-DEX
38
- equity is collapsed via **`max()` not `sum()`** — one cross-margined wallet,
39
- two sub-DEX views (summing double-counts the shared free balance → 2x sizing).
40
- - **leverage** = `min(config leverage 3, MAX_LEVERAGE 5)`.
41
-
42
- MCP tools used (all **read-only**): `market_get_asset_data` (4h candles),
43
- `strategy_get_clearinghouse_state`.
44
-
45
- State (the per-coin **240s recent-signal** dedup map) lives in `ctx.state` (the
46
- source kept it in `recent-signals.json`).
47
-
48
- ## Why iguana emits at most one signal per tick
49
-
50
- Turbine/spider emit **all** gated candidates and let the runtime apply the slot
51
- ceiling. **Iguana is deliberately different: it emits ≤ 1 signal per tick** — the
52
- single strongest index trend. This is the **strategy**, not an oversight:
53
- iguana's thesis is literally "two assets, **one decision per tick**," and the
54
- source producer picked the single strongest trend and pushed exactly one signal.
55
- `slots: 1` matches the single-position thesis.
56
-
57
- ## Layout
58
-
59
- ```
60
- iguana/
61
- ├── README.md
62
- ├── recipe.yaml # the single recipe (path: ./scanners, entrypoint: scan.py)
63
- └── scanners/
64
- ├── scan.py # scan(inputs, ctx) entrypoint (index-trend pipeline)
65
- └── scoring.py # pure trend/scoring functions (unit-tested, ported verbatim)
66
- ```
67
-
68
- ## Environment variables
69
-
70
- | Variable | Required | Purpose |
71
- |--------------------|----------|---------|
72
- | `IGUANA_WALLET` | yes | Iguana's strategy wallet |
73
- | `SENPI_API_KEY` | yes | MCP auth for `ctx.senpi_mcp` (injected into the scaffold child) |
74
- | `SENPI_MCP_URL` | yes | MCP server URL |
75
- | `TELEGRAM_CHAT_ID` | optional | Notifications |
76
-
77
- ## Install / run
78
-
79
- ```bash
80
- IGUANA_WALLET=0x... \
81
- SENPI_API_KEY=... SENPI_MCP_URL=https://mcp.prod.senpi.ai/mcp \
82
- openclaw senpi run examples/strategies/iguana/recipe.yaml
83
- ```
84
-
85
- ## Fidelity notes
86
-
87
- Source behaviors that changed (or did not map 1:1) under the v2 contract.
88
- Nothing load-bearing was silently dropped.
89
-
90
- ### Dropped: the daemon loop + ingest POST
91
-
92
- The source ran a `producer_daemon(interval_seconds=300)` that POSTed signals to
93
- the ingest endpoint via `push_signal`. Under v2 the runtime **supervises** this
94
- module and calls `scan()` once per `interval_seconds`; `scan()` is **single-pass
95
- and synchronous** and **returns a `list[dict]`**. The scaffold owns delivery, the
96
- wire envelope, and `signal_id` dedup. The source's normalized [0,1] wire score
97
- (`min(score / 6, 1.0)`) is **not** recomputed in `scan()` — the raw additive
98
- score rides on `data{}` and the runtime owns wire scoring. (The `6.0` divisor is
99
- preserved in `scoring.py` for parity.)
100
-
101
- ### Moved to `ctx.state`: the 240s recent-signal dedup
102
-
103
- The per-coin **240s** recent-signal dedup that the source kept in
104
- `recent-signals.json` now lives in `ctx.state`: each tick reads the latest
105
- `{coin: ts}` map, **prunes** it by the source window (`4 × TTL`), **skips** coins
106
- signaled within the TTL, **stamps** the chosen candidate, and appends the updated
107
- map for the next tick.
108
-
109
- ### Action gate: `decision_mode: llm` → `rule`
110
-
111
- The source `OPEN_POSITION` action used `decision_mode: llm` with a pass-through
112
- prompt that copied the signal's `asset`/`direction`/`leverage`/`marginUsd`
113
- verbatim and pinned confidence — a rule wearing an LLM costume. This recipe uses
114
- `decision_mode: rule` directly.
115
-
116
- ### DSL preset maps cleanly
117
-
118
- The source DSL preset maps 1:1 to the v2 schema (no dropped tiers): hard_timeout
119
- 2880m (48h), weak_peak 480m@3, dead_weight **disabled**, phase1 max_loss 12 /
120
- retrace 8, and a 5-tier phase2 (5/0 · 10/40 · 18/60 · 30/75 · 50/85) — all
121
- `trigger_pct ≤ 100` and the T0 `lock_hw_pct 0` is in the allowed `[0, 100]` range.
122
- `dead_weight_cut` is disabled (`enabled: false`) with an inert
123
- `interval_in_minutes` placeholder so the parser is satisfied.
124
-
125
- ### Guard rails: minutes/hours → seconds
126
-
127
- The v2 schema uses integer seconds everywhere. `cooldown_minutes 90` →
128
- `cooldown_seconds 5400`; `per_asset_cooldown_minutes 360` →
129
- `per_asset_cooldown_seconds 21600` (clears the schema's 300s floor);
130
- `data_retention_hours 96` → `data_retention_seconds 345600`.
131
-
132
- ### Only read-only MCP calls
133
-
134
- `scan.py` never opens, closes, or cancels — it produces signals. A test asserts
135
- no `create_position` / `close_position` / `cancel_order` tool is ever called.
@@ -1,137 +0,0 @@
1
- # ── IGUANA (v2 supervised) ───────────────────────────────────────────
2
- # XYZ macro index trend — the simplest possible XYZ equity exposure.
3
- # SINGLE wallet, SINGLE recipe, ONE supervised scanner.
4
- #
5
- # Ported from senpi-skills/iguana (producer v1.0.1, config v1.0.0) to the v2
6
- # supervised external-scanner contract. The producer daemon is gone: the runtime
7
- # supervises ./scanners/scan.py and calls scan(inputs, ctx) every
8
- # `interval_seconds` (300s). All gates + scoring preserved exactly; the pure
9
- # trend/scoring functions are ported VERBATIM (they were already unit-tested in
10
- # the source). See scan.py + scoring.py.
11
- name: iguana-tracker
12
- version: 2.0.0
13
- group: iguana # agent-facing handle (harmless for single-wallet)
14
- description: >
15
- IGUANA — XYZ Macro Index Trend. "I want stock-market exposure on Hyperliquid
16
- without picking stocks." Trend-follows the two broad XYZ indices xyz:SP500 +
17
- xyz:XYZ100 — two assets, ONE decision per tick. Each tick computes a 4-day
18
- trend strength (% change of the latest 4h close vs 24 bars ago) per index,
19
- picks the stronger move past minTrendPct (1.5%), scores it (base 3, +2 if the
20
- move is strong >= 4%, +1 if volume is rising), and emits the SINGLE strongest
21
- index trend in its direction if score >= minScore (4). Conservative sizing
22
- (20% margin, 3x). DSL: 48h hard_timeout (XYZ weekend pricing-gap risk),
23
- weak_peak 8h@3, 5-tier phase2 ladder. ONBOARDING TIER.
24
-
25
- strategy:
26
- wallet: "${IGUANA_WALLET}"
27
- slots: 1 # source slots 1 (iguana emits <=1 signal/tick — two assets, one decision)
28
- margin_pct: 20 # source margin_pct 20 (% of account per slot; scan.py carries the exact marginUsd)
29
- default_leverage: 3 # source leverage 3 (capped at MAX_LEVERAGE 5 in scan.py)
30
- trading_risk: conservative # source trading_risk conservative
31
- enabled: true
32
-
33
- scanners:
34
- - name: position_tracker # built-in: feeds DSL exit engine
35
- type: position_tracker
36
- interval_seconds: 10 # source position_tracker interval 10s
37
-
38
- - name: iguana_signals # supervised external scanner
39
- type: external_scanner
40
- path: ./scanners
41
- entrypoint: scan.py
42
- interval_seconds: 300 # source producer_daemon interval_seconds 300 (5min)
43
- timeout_seconds: 180 # source tick_timeout 180
44
- default_signal_validity_seconds: 600 # 2x tick; pass-through gate re-scores next tick
45
- state_history_max_count: 200 # holds the per-coin 240s recent-signal dedup map records
46
- inputs:
47
- # ── universe + trend gates (config v1.0.0) ──
48
- whitelist: ["xyz:SP500", "xyz:XYZ100"] # the two broad XYZ indices
49
- trendLookbackBars: 24 # 24 × 4h bars = 4 days
50
- minTrendPct: 1.5 # minimum |4-day move| to call it a trend
51
- strongTrendPct: 4.0 # |move| >= this scores the strong-trend bonus (+2)
52
- minScore: 4 # additive-score floor to emit
53
- # ── sizing (config v1.0.0) ──
54
- marginPct: 0.20 # margin = account_value * 0.20
55
- leverage: 3 # leverage (capped at 5 in scan.py)
56
- signal_data_schema:
57
- score: { type: number }
58
- direction: { type: string }
59
- reasons: { type: array, required: false }
60
- trendPct: { type: number, required: false }
61
- volumeTrendPct: { type: number, required: false }
62
- heldAssets: { type: array, required: false }
63
-
64
- actions:
65
- - name: position_tracker_action # rule-based lifecycle → DSL
66
- action_type: POSITION_TRACKER
67
- decision_mode: rule
68
- scanners: [position_tracker]
69
-
70
- - name: iguana_entry # OPEN_POSITION, rule mode (source LLM gate was pure pass-through)
71
- action_type: OPEN_POSITION
72
- decision_mode: rule
73
- scanners: [iguana_signals]
74
- params:
75
- order_type: FEE_OPTIMIZED_LIMIT
76
- fee_optimized_limit_options:
77
- ensure_execution_as_taker: true # source entry ensureExecutionAsTaker true — ride the trend NOW
78
- execution_timeout_seconds: 30
79
- context:
80
- - type: signal
81
- scanner: iguana_signals
82
-
83
- # ── EXIT (DSL — balanced, XYZ-tuned; preset preserved from source v1.0.0) ──
84
- #
85
- # Index trends extend over days/weeks; the 48h hard_timeout accommodates the
86
- # weekend pricing-gap risk on XYZ. weak_peak_cut bails from flat-but-tired
87
- # positions before the weekend gap. dead_weight_cut is disabled (source).
88
- exit:
89
- engine: dsl
90
- interval_seconds: 60 # source exit interval_seconds 60
91
- order_type: FEE_OPTIMIZED_LIMIT
92
- fee_optimized_limit_options:
93
- ensure_execution_as_taker: true
94
- execution_timeout_seconds: 30
95
- dsl_preset:
96
- hard_timeout:
97
- enabled: true
98
- interval_in_minutes: 2880 # source hard_timeout 2880min (48h — XYZ weekend pricing-gap risk)
99
- weak_peak_cut:
100
- enabled: true
101
- interval_in_minutes: 480 # source weak_peak 480min (8h)
102
- min_value: 3.0 # source weak_peak min 3.0%
103
- dead_weight_cut:
104
- enabled: false # source disabled
105
- interval_in_minutes: 60 # inert (enabled:false) — placeholder for the parser
106
- phase1:
107
- enabled: true
108
- max_loss_pct: 12.0 # source phase1 max_loss 12
109
- retrace_threshold: 8 # source phase1 retrace 8
110
- consecutive_breaches_required: 1
111
- phase2:
112
- enabled: true
113
- tiers: # source T0..T4 — carried verbatim (all trigger_pct <= 100)
114
- - { trigger_pct: 5, lock_hw_pct: 0 }
115
- - { trigger_pct: 10, lock_hw_pct: 40 }
116
- - { trigger_pct: 18, lock_hw_pct: 60 }
117
- - { trigger_pct: 30, lock_hw_pct: 75 }
118
- - { trigger_pct: 50, lock_hw_pct: 85 }
119
-
120
- # ── RISK guard rails (from source runtime.yaml; minutes/hours → seconds) ──
121
- risk:
122
- data_retention_seconds: 345600 # 96h (was data_retention_hours 96)
123
- guard_rails:
124
- daily_loss_limit_pct: 12
125
- max_entries_per_day: 2
126
- bypass_max_entries_per_day_on_profit: false
127
- max_consecutive_losses: 3
128
- cooldown_seconds: 5400 # was cooldown_minutes 90
129
- drawdown_halt_pct: 20
130
- drawdown_reset_on_day_rollover: false
131
- per_asset_cooldown_seconds: 21600 # was per_asset_cooldown_minutes 360 (6h)
132
-
133
- notifications:
134
- telegram_chat_id: "${TELEGRAM_CHAT_ID}"
135
- dsl_lifecycle: true
136
- dsl_notify_sl_updates: false
137
- action_lifecycle: true