@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.
- package/README.md +9 -10
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +0 -3
- package/dist/actions/decision-engine.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 +2 -6
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +56 -43
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +108 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +0 -2
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +0 -2
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/engine-client/config-mapping.d.ts +0 -7
- package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/config-mapping.js +2 -16
- package/dist/dsl/engine-client/config-mapping.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +2 -18
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +213 -302
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +0 -15
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +9 -44
- 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 +1 -24
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +1 -16
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +4 -35
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/dsl/plugin/options.d.ts +0 -18
- package/dist/dsl/plugin/options.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +3 -4
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +5 -14
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.js +1 -12
- package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
- package/dist/runtime/load-runtime-config.js +1 -1
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/map-strategies-registration.js +8 -7
- package/dist/runtime/map-strategies-registration.js.map +1 -1
- package/dist/runtime/map-strategies.d.ts.map +1 -1
- package/dist/runtime/map-strategies.js +3 -12
- package/dist/runtime/map-strategies.js.map +1 -1
- package/dist/runtime/reconcile-state.d.ts +0 -44
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +44 -80
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/risk-guard.d.ts.map +1 -1
- package/dist/runtime/risk-guard.js +12 -17
- package/dist/runtime/risk-guard.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +995 -2855
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +40 -133
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +0 -26
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +5 -233
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scanner-registry.d.ts +1 -1
- package/dist/runtime/scanner-registry.d.ts.map +1 -1
- package/dist/runtime/scanner-registry.js +12 -3
- package/dist/runtime/scanner-registry.js.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +36 -28
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +0 -0
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts +4 -18
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +3 -24
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +0 -6
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +0 -5
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/scanners/external/liquidation-watchdog/README.md +107 -0
- package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +48 -0
- package/dist/scanners/external/liquidation-watchdog/producer.mjs +336 -0
- package/dist/scanners/external/momentum/README.md +148 -0
- package/dist/scanners/external/momentum/momentum.yaml +116 -0
- package/dist/scanners/external/momentum/producer.mjs +251 -0
- package/dist/scanners/external/scaffold/__main__.py +24 -65
- package/dist/scanners/external/scaffold/delivery.py +22 -56
- package/dist/scanners/external/scaffold/entrypoint_loader.py +48 -42
- package/dist/scanners/external/scaffold/journal.py +2 -13
- package/dist/scanners/external/scaffold/scaffold.py +128 -159
- package/dist/scanners/external/scaffold/signal_envelope.py +34 -177
- package/dist/scanners/external/shared/ingest.mjs +112 -0
- package/dist/scanners/external-scanner-receiver.d.ts +0 -6
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +1 -14
- package/dist/scanners/external-scanner-receiver.js.map +1 -1
- package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
- package/dist/scripts/run-dsl-standalone-live.js +3 -5
- package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
- package/dist/senpi/client.d.ts +0 -12
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +2 -31
- package/dist/senpi/client.js.map +1 -1
- package/dist/state/state-manager.d.ts +0 -17
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +0 -28
- package/dist/state/state-manager.js.map +1 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/strategy/strategy-state.js +1 -7
- package/dist/strategy/strategy-state.js.map +1 -1
- package/dist/tracking/constants.d.ts.map +1 -1
- package/dist/tracking/constants.js +0 -2
- package/dist/tracking/constants.js.map +1 -1
- package/dist/types/dsl/index.d.ts +1 -1
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/types/scanner.d.ts +0 -7
- package/dist/types/scanner.d.ts.map +1 -1
- package/examples/README.md +89 -0
- package/examples/scanners-consumer-quickstart.mjs +134 -0
- package/examples/scanners-emerging-movers-v4.mjs +287 -0
- package/examples/scanners-liquidation-watchdog.mjs +341 -0
- package/examples/scanners-live-mcp.mjs +208 -0
- package/examples/scanners-multi-strategy.mjs +165 -0
- package/examples/scanners-position-tracker-e2e.mjs +225 -0
- package/examples/scanners-scheduled-events.mjs +96 -0
- package/examples/scanners-senpi-provider-client.mjs +182 -0
- package/examples/scanners-single-strategy.mjs +198 -0
- package/examples/strategies/README.md +12 -17
- package/examples/strategies/dsl-showcase.yaml +68 -0
- package/examples/strategies/dsl.yaml +51 -0
- package/examples/strategies/fox-minimal-loosened.yaml +240 -0
- package/examples/strategies/fox-minimal.yaml +242 -0
- package/examples/strategies/minimal.yaml +46 -0
- package/examples/strategies/momentum/how-it-works.md +48 -0
- package/examples/strategies/momentum/momentum-guarded.yaml +161 -0
- package/examples/strategies/risk-showcase.yaml +93 -0
- package/examples/strategies/viper.yaml +67 -0
- package/examples/strategies/wolf.yaml +72 -0
- package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +123 -0
- package/package.json +1 -1
- package/dist/runtime/reconcile-decide.d.ts +0 -73
- package/dist/runtime/reconcile-decide.d.ts.map +0 -1
- package/dist/runtime/reconcile-decide.js +0 -166
- package/dist/runtime/reconcile-decide.js.map +0 -1
- package/dist/runtime/reconcile-run.d.ts +0 -62
- package/dist/runtime/reconcile-run.d.ts.map +0 -1
- package/dist/runtime/reconcile-run.js +0 -173
- package/dist/runtime/reconcile-run.js.map +0 -1
- package/dist/scanners/external/scaffold/ctx.py +0 -78
- package/dist/scanners/external/scaffold/requirements.txt +0 -11
- package/dist/scanners/external/scaffold/senpi_mcp.py +0 -250
- package/dist/scanners/external/scaffold/state_manager.py +0 -175
- package/dist/utils/keyed-mutex.d.ts +0 -37
- package/dist/utils/keyed-mutex.d.ts.map +0 -1
- package/dist/utils/keyed-mutex.js +0 -77
- package/dist/utils/keyed-mutex.js.map +0 -1
- package/examples/strategies/iguana/README.md +0 -135
- package/examples/strategies/iguana/recipe.yaml +0 -137
- package/examples/strategies/iguana/scanners/scan.py +0 -243
- package/examples/strategies/iguana/scanners/scoring.py +0 -152
- package/examples/strategies/raptor/README.md +0 -156
- package/examples/strategies/raptor/recipe.yaml +0 -154
- package/examples/strategies/raptor/scanners/scan.py +0 -349
- package/examples/strategies/raptor/scanners/scoring.py +0 -452
- package/examples/strategies/spider/README.md +0 -97
- package/examples/strategies/spider/scalp/recipe.yaml +0 -129
- package/examples/strategies/spider/scalp/scanners/scan.py +0 -219
- package/examples/strategies/spider/scalp/scanners/scoring.py +0 -179
- package/examples/strategies/spider/swing/recipe.yaml +0 -141
- package/examples/strategies/spider/swing/scanners/scan.py +0 -334
- package/examples/strategies/spider/swing/scanners/scoring.py +0 -184
- package/examples/strategies/turbine/README.md +0 -167
- package/examples/strategies/turbine/recipe-runners.yaml +0 -123
- package/examples/strategies/turbine/recipe-volume.yaml +0 -129
- package/examples/strategies/turbine/scanners/scan.py +0 -283
- 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
|