@reefclaw/connect 0.1.12 → 0.1.14

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 (391) hide show
  1. package/assets/plugin/audit/mode-transition-audit.d.ts +11 -0
  2. package/assets/plugin/audit/mode-transition-audit.js +29 -0
  3. package/assets/plugin/balance-utils.d.ts +36 -0
  4. package/assets/plugin/balance-utils.js +98 -0
  5. package/assets/plugin/ccxt/binance-ban-gate.d.ts +47 -0
  6. package/assets/plugin/ccxt/binance-ban-gate.js +420 -0
  7. package/assets/plugin/ccxt/binance-private.d.ts +346 -0
  8. package/assets/plugin/ccxt/binance-private.js +1525 -0
  9. package/assets/plugin/ccxt/binance-public.d.ts +34 -0
  10. package/assets/plugin/ccxt/binance-public.js +180 -0
  11. package/assets/plugin/ccxt/intel-public.d.ts +25 -0
  12. package/assets/plugin/ccxt/intel-public.js +80 -0
  13. package/assets/plugin/ccxt/public-market-data-api.d.ts +26 -0
  14. package/assets/plugin/ccxt/public-market-data-api.js +23 -0
  15. package/assets/plugin/config/agent-config-client.d.ts +77 -0
  16. package/assets/plugin/config/agent-config-client.js +188 -0
  17. package/assets/plugin/config/agent-config-poller.d.ts +32 -0
  18. package/assets/plugin/config/agent-config-poller.js +101 -0
  19. package/assets/plugin/config/brackets-config.d.ts +22 -0
  20. package/assets/plugin/config/brackets-config.js +58 -0
  21. package/assets/plugin/config/entitlement-gate.d.ts +51 -0
  22. package/assets/plugin/config/entitlement-gate.js +137 -0
  23. package/assets/plugin/config/gate-store.d.ts +18 -0
  24. package/assets/plugin/config/gate-store.js +61 -0
  25. package/assets/plugin/config/plugin-config-io.d.ts +222 -0
  26. package/assets/plugin/config/plugin-config-io.js +126 -0
  27. package/assets/plugin/config/position-review-config.d.ts +35 -0
  28. package/assets/plugin/config/position-review-config.js +105 -0
  29. package/assets/plugin/config/tool-gate.d.ts +53 -0
  30. package/assets/plugin/config/tool-gate.js +128 -0
  31. package/assets/plugin/config/user-data-stream-config.d.ts +89 -0
  32. package/assets/plugin/config/user-data-stream-config.js +239 -0
  33. package/assets/plugin/connector-supervisor.d.ts +36 -0
  34. package/assets/plugin/connector-supervisor.js +149 -0
  35. package/assets/plugin/exchange-adapter.d.ts +65 -0
  36. package/assets/plugin/exchange-adapter.js +4 -0
  37. package/assets/plugin/index.d.ts +30 -0
  38. package/assets/plugin/index.js +2763 -0
  39. package/assets/plugin/ingest/pending-entry-metadata.d.ts +74 -0
  40. package/assets/plugin/ingest/pending-entry-metadata.js +236 -0
  41. package/assets/plugin/ingest/position-auto-capture.d.ts +171 -0
  42. package/assets/plugin/ingest/position-auto-capture.js +709 -0
  43. package/assets/plugin/ingest/position-decisions-client.d.ts +327 -0
  44. package/assets/plugin/ingest/position-decisions-client.js +306 -0
  45. package/assets/plugin/ingest/readiness-reporter.d.ts +42 -0
  46. package/assets/plugin/ingest/readiness-reporter.js +163 -0
  47. package/assets/plugin/ingest/reconcile-db-vs-exchange.d.ts +51 -0
  48. package/assets/plugin/ingest/reconcile-db-vs-exchange.js +170 -0
  49. package/assets/plugin/ingest/reconciler-cleanup.d.ts +37 -0
  50. package/assets/plugin/ingest/reconciler-cleanup.js +147 -0
  51. package/assets/plugin/ingest/rest-gap-filler.d.ts +191 -0
  52. package/assets/plugin/ingest/rest-gap-filler.js +565 -0
  53. package/assets/plugin/ingest/touched-symbols-store.d.ts +25 -0
  54. package/assets/plugin/ingest/touched-symbols-store.js +96 -0
  55. package/assets/plugin/ingest/trade-store-client.d.ts +40 -0
  56. package/assets/plugin/ingest/trade-store-client.js +125 -0
  57. package/assets/plugin/ingest/ws-ingest.d.ts +43 -0
  58. package/assets/plugin/ingest/ws-ingest.js +126 -0
  59. package/assets/plugin/learning/setup-family.d.ts +21 -0
  60. package/assets/plugin/learning/setup-family.js +103 -0
  61. package/assets/plugin/lifecycle/install-signal-handlers.d.ts +33 -0
  62. package/assets/plugin/lifecycle/install-signal-handlers.js +112 -0
  63. package/assets/plugin/lifecycle/shutdown-coordinator.d.ts +43 -0
  64. package/assets/plugin/lifecycle/shutdown-coordinator.js +131 -0
  65. package/assets/plugin/lifecycle/trading-operation-lock.d.ts +17 -0
  66. package/assets/plugin/lifecycle/trading-operation-lock.js +14 -0
  67. package/assets/plugin/live/bracket-id.d.ts +26 -0
  68. package/assets/plugin/live/bracket-id.js +112 -0
  69. package/assets/plugin/live/bracket-ledger.d.ts +54 -0
  70. package/assets/plugin/live/bracket-ledger.js +267 -0
  71. package/assets/plugin/live/bracket-manager.d.ts +82 -0
  72. package/assets/plugin/live/bracket-manager.js +478 -0
  73. package/assets/plugin/live/bracket-params.d.ts +22 -0
  74. package/assets/plugin/live/bracket-params.js +124 -0
  75. package/assets/plugin/live/bracket-reconciler.d.ts +95 -0
  76. package/assets/plugin/live/bracket-reconciler.js +573 -0
  77. package/assets/plugin/live/bracket-types.d.ts +102 -0
  78. package/assets/plugin/live/bracket-types.js +8 -0
  79. package/assets/plugin/live/deposit-tracker.d.ts +62 -0
  80. package/assets/plugin/live/deposit-tracker.js +97 -0
  81. package/assets/plugin/live/emergency-controls.d.ts +32 -0
  82. package/assets/plugin/live/emergency-controls.js +226 -0
  83. package/assets/plugin/live/exchange-errors.d.ts +12 -0
  84. package/assets/plugin/live/exchange-errors.js +130 -0
  85. package/assets/plugin/live/exchange-info-cache.d.ts +37 -0
  86. package/assets/plugin/live/exchange-info-cache.js +134 -0
  87. package/assets/plugin/live/fact-subscriber.d.ts +78 -0
  88. package/assets/plugin/live/fact-subscriber.js +182 -0
  89. package/assets/plugin/live/fill-price.d.ts +13 -0
  90. package/assets/plugin/live/fill-price.js +37 -0
  91. package/assets/plugin/live/intent-journal.d.ts +42 -0
  92. package/assets/plugin/live/intent-journal.js +122 -0
  93. package/assets/plugin/live/listen-key-manager.d.ts +70 -0
  94. package/assets/plugin/live/listen-key-manager.js +169 -0
  95. package/assets/plugin/live/live-adapter.d.ts +319 -0
  96. package/assets/plugin/live/live-adapter.js +1906 -0
  97. package/assets/plugin/live/live-balance-enricher.d.ts +32 -0
  98. package/assets/plugin/live/live-balance-enricher.js +104 -0
  99. package/assets/plugin/live/live-bracket-api.d.ts +13 -0
  100. package/assets/plugin/live/live-bracket-api.js +20 -0
  101. package/assets/plugin/live/live-state-store.d.ts +194 -0
  102. package/assets/plugin/live/live-state-store.js +450 -0
  103. package/assets/plugin/live/local-signal-service.d.ts +57 -0
  104. package/assets/plugin/live/local-signal-service.js +151 -0
  105. package/assets/plugin/live/local-strategy-evaluator.d.ts +62 -0
  106. package/assets/plugin/live/local-strategy-evaluator.js +131 -0
  107. package/assets/plugin/live/microstructure-assembler.d.ts +54 -0
  108. package/assets/plugin/live/microstructure-assembler.js +148 -0
  109. package/assets/plugin/live/order-poller.d.ts +29 -0
  110. package/assets/plugin/live/order-poller.js +125 -0
  111. package/assets/plugin/live/position-state-store.d.ts +87 -0
  112. package/assets/plugin/live/position-state-store.js +237 -0
  113. package/assets/plugin/live/proposal-decision-listener.d.ts +70 -0
  114. package/assets/plugin/live/proposal-decision-listener.js +292 -0
  115. package/assets/plugin/live/proposal-manager.d.ts +76 -0
  116. package/assets/plugin/live/proposal-manager.js +140 -0
  117. package/assets/plugin/live/rate-limiter.d.ts +47 -0
  118. package/assets/plugin/live/rate-limiter.js +159 -0
  119. package/assets/plugin/live/reconciler.d.ts +39 -0
  120. package/assets/plugin/live/reconciler.js +175 -0
  121. package/assets/plugin/live/setup-buckets.d.ts +7 -0
  122. package/assets/plugin/live/setup-buckets.js +33 -0
  123. package/assets/plugin/live/slippage-tracker.d.ts +45 -0
  124. package/assets/plugin/live/slippage-tracker.js +78 -0
  125. package/assets/plugin/live/stop-watcher.d.ts +67 -0
  126. package/assets/plugin/live/stop-watcher.js +218 -0
  127. package/assets/plugin/live/user-data-active-probe.d.ts +54 -0
  128. package/assets/plugin/live/user-data-active-probe.js +180 -0
  129. package/assets/plugin/live/user-data-stream-controller.d.ts +200 -0
  130. package/assets/plugin/live/user-data-stream-controller.js +579 -0
  131. package/assets/plugin/live/user-data-stream-ws.d.ts +22 -0
  132. package/assets/plugin/live/user-data-stream-ws.js +63 -0
  133. package/assets/plugin/live/user-data-stream.d.ts +243 -0
  134. package/assets/plugin/live/user-data-stream.js +704 -0
  135. package/assets/plugin/logger.d.ts +2 -0
  136. package/assets/plugin/logger.js +2 -0
  137. package/assets/plugin/mfe.d.ts +21 -0
  138. package/assets/plugin/mfe.js +68 -0
  139. package/assets/plugin/onboarding/mode-ladder.d.ts +1 -0
  140. package/assets/plugin/onboarding/mode-ladder.js +3 -0
  141. package/assets/plugin/onboarding/runtime.d.ts +102 -0
  142. package/assets/plugin/onboarding/runtime.js +215 -0
  143. package/assets/plugin/openclaw.plugin.json +92 -0
  144. package/assets/plugin/paper-adapter.d.ts +24 -0
  145. package/assets/plugin/paper-adapter.js +91 -0
  146. package/assets/plugin/persistence/state-manager.d.ts +66 -0
  147. package/assets/plugin/persistence/state-manager.js +222 -0
  148. package/assets/plugin/pinned-plan.d.ts +9 -0
  149. package/assets/plugin/pinned-plan.js +23 -0
  150. package/assets/plugin/portfolio/reentry-tracker.d.ts +36 -0
  151. package/assets/plugin/portfolio/reentry-tracker.js +127 -0
  152. package/assets/plugin/portfolio/wave9-admission.d.ts +67 -0
  153. package/assets/plugin/portfolio/wave9-admission.js +262 -0
  154. package/assets/plugin/portfolio/wave9-policy.d.ts +36 -0
  155. package/assets/plugin/portfolio/wave9-policy.js +183 -0
  156. package/assets/plugin/risk/pre-trade-check.d.ts +38 -0
  157. package/assets/plugin/risk/pre-trade-check.js +345 -0
  158. package/assets/plugin/risk/pre-trade-types.d.ts +60 -0
  159. package/assets/plugin/risk/pre-trade-types.js +3 -0
  160. package/assets/plugin/shadow/shadow-tracker.d.ts +36 -0
  161. package/assets/plugin/shadow/shadow-tracker.js +151 -0
  162. package/assets/plugin/shadow/types.d.ts +42 -0
  163. package/assets/plugin/shadow/types.js +20 -0
  164. package/assets/plugin/shared/indicators-extended.d.ts +52 -0
  165. package/assets/plugin/shared/indicators-extended.js +291 -0
  166. package/assets/plugin/shared/indicators.d.ts +15 -0
  167. package/assets/plugin/shared/indicators.js +114 -0
  168. package/assets/plugin/signals/conditions/registry.d.ts +16 -0
  169. package/assets/plugin/signals/conditions/registry.js +1333 -0
  170. package/assets/plugin/signals/conditions/types.d.ts +1 -0
  171. package/assets/plugin/signals/conditions/types.js +4 -0
  172. package/assets/plugin/signals/direction-rules.d.ts +3 -0
  173. package/assets/plugin/signals/direction-rules.js +24 -0
  174. package/assets/plugin/signals/entry-rules.d.ts +6 -0
  175. package/assets/plugin/signals/entry-rules.js +33 -0
  176. package/assets/plugin/signals/serialize-context.d.ts +4 -0
  177. package/assets/plugin/signals/serialize-context.js +39 -0
  178. package/assets/plugin/signals/stop-rules.d.ts +3 -0
  179. package/assets/plugin/signals/stop-rules.js +48 -0
  180. package/assets/plugin/signals/strategy-adapter.d.ts +47 -0
  181. package/assets/plugin/signals/strategy-adapter.js +209 -0
  182. package/assets/plugin/signals/types.d.ts +1 -0
  183. package/assets/plugin/signals/types.js +8 -0
  184. package/assets/plugin/simulator/exchange-simulator.d.ts +105 -0
  185. package/assets/plugin/simulator/exchange-simulator.js +759 -0
  186. package/assets/plugin/simulator/fill-engine.d.ts +53 -0
  187. package/assets/plugin/simulator/fill-engine.js +280 -0
  188. package/assets/plugin/simulator/paper-market-feed.d.ts +26 -0
  189. package/assets/plugin/simulator/paper-market-feed.js +104 -0
  190. package/assets/plugin/simulator/realistic-fills.d.ts +59 -0
  191. package/assets/plugin/simulator/realistic-fills.js +175 -0
  192. package/assets/plugin/simulator/types.d.ts +228 -0
  193. package/assets/plugin/simulator/types.js +43 -0
  194. package/assets/plugin/strategy/builtin-strategies.d.ts +2 -0
  195. package/assets/plugin/strategy/builtin-strategies.js +113 -0
  196. package/assets/plugin/strategy/condition-registry.d.ts +3 -0
  197. package/assets/plugin/strategy/condition-registry.js +153 -0
  198. package/assets/plugin/strategy/evaluator.d.ts +70 -0
  199. package/assets/plugin/strategy/evaluator.js +98 -0
  200. package/assets/plugin/tools/assessment-validation.d.ts +141 -0
  201. package/assets/plugin/tools/assessment-validation.js +473 -0
  202. package/assets/plugin/tools/attach-brackets.d.ts +39 -0
  203. package/assets/plugin/tools/attach-brackets.js +564 -0
  204. package/assets/plugin/tools/audit-bracket-protection.d.ts +49 -0
  205. package/assets/plugin/tools/audit-bracket-protection.js +683 -0
  206. package/assets/plugin/tools/bracket-control.d.ts +12 -0
  207. package/assets/plugin/tools/bracket-control.js +35 -0
  208. package/assets/plugin/tools/cancel-all-orders.d.ts +9 -0
  209. package/assets/plugin/tools/cancel-all-orders.js +8 -0
  210. package/assets/plugin/tools/cancel-order.d.ts +14 -0
  211. package/assets/plugin/tools/cancel-order.js +60 -0
  212. package/assets/plugin/tools/check-position-health.d.ts +46 -0
  213. package/assets/plugin/tools/check-position-health.js +194 -0
  214. package/assets/plugin/tools/clear-exchange-credentials.d.ts +24 -0
  215. package/assets/plugin/tools/clear-exchange-credentials.js +70 -0
  216. package/assets/plugin/tools/close-position.d.ts +45 -0
  217. package/assets/plugin/tools/close-position.js +722 -0
  218. package/assets/plugin/tools/create-order.d.ts +89 -0
  219. package/assets/plugin/tools/create-order.js +1555 -0
  220. package/assets/plugin/tools/exit-gate.d.ts +58 -0
  221. package/assets/plugin/tools/exit-gate.js +162 -0
  222. package/assets/plugin/tools/fetch-balance.d.ts +5 -0
  223. package/assets/plugin/tools/fetch-balance.js +4 -0
  224. package/assets/plugin/tools/fetch-ohlcv.d.ts +11 -0
  225. package/assets/plugin/tools/fetch-ohlcv.js +8 -0
  226. package/assets/plugin/tools/fetch-open-orders.d.ts +7 -0
  227. package/assets/plugin/tools/fetch-open-orders.js +4 -0
  228. package/assets/plugin/tools/fetch-positions.d.ts +7 -0
  229. package/assets/plugin/tools/fetch-positions.js +4 -0
  230. package/assets/plugin/tools/fetch-ticker.d.ts +11 -0
  231. package/assets/plugin/tools/fetch-ticker.js +5 -0
  232. package/assets/plugin/tools/get-agent-profile.d.ts +4 -0
  233. package/assets/plugin/tools/get-agent-profile.js +6 -0
  234. package/assets/plugin/tools/get-analytics.d.ts +6 -0
  235. package/assets/plugin/tools/get-analytics.js +7 -0
  236. package/assets/plugin/tools/get-backtest.d.ts +12 -0
  237. package/assets/plugin/tools/get-backtest.js +91 -0
  238. package/assets/plugin/tools/get-basis.d.ts +7 -0
  239. package/assets/plugin/tools/get-basis.js +7 -0
  240. package/assets/plugin/tools/get-bracket-config.d.ts +11 -0
  241. package/assets/plugin/tools/get-bracket-config.js +24 -0
  242. package/assets/plugin/tools/get-cascade-risk.d.ts +7 -0
  243. package/assets/plugin/tools/get-cascade-risk.js +8 -0
  244. package/assets/plugin/tools/get-crypto-metrics.d.ts +18 -0
  245. package/assets/plugin/tools/get-crypto-metrics.js +62 -0
  246. package/assets/plugin/tools/get-cvd.d.ts +6 -0
  247. package/assets/plugin/tools/get-cvd.js +6 -0
  248. package/assets/plugin/tools/get-divergences.d.ts +6 -0
  249. package/assets/plugin/tools/get-divergences.js +6 -0
  250. package/assets/plugin/tools/get-funding-context.d.ts +6 -0
  251. package/assets/plugin/tools/get-funding-context.js +16 -0
  252. package/assets/plugin/tools/get-liquidation-levels.d.ts +7 -0
  253. package/assets/plugin/tools/get-liquidation-levels.js +7 -0
  254. package/assets/plugin/tools/get-liquidation-pulse.d.ts +9 -0
  255. package/assets/plugin/tools/get-liquidation-pulse.js +22 -0
  256. package/assets/plugin/tools/get-market-breadth.d.ts +6 -0
  257. package/assets/plugin/tools/get-market-breadth.js +8 -0
  258. package/assets/plugin/tools/get-market-intel.d.ts +19 -0
  259. package/assets/plugin/tools/get-market-intel.js +116 -0
  260. package/assets/plugin/tools/get-market-structure.d.ts +47 -0
  261. package/assets/plugin/tools/get-market-structure.js +198 -0
  262. package/assets/plugin/tools/get-my-mined-patterns.d.ts +20 -0
  263. package/assets/plugin/tools/get-my-mined-patterns.js +61 -0
  264. package/assets/plugin/tools/get-my-proposed-learnings.d.ts +20 -0
  265. package/assets/plugin/tools/get-my-proposed-learnings.js +55 -0
  266. package/assets/plugin/tools/get-my-recent-reviews.d.ts +22 -0
  267. package/assets/plugin/tools/get-my-recent-reviews.js +66 -0
  268. package/assets/plugin/tools/get-orderbook.d.ts +21 -0
  269. package/assets/plugin/tools/get-orderbook.js +32 -0
  270. package/assets/plugin/tools/get-pattern-scan.d.ts +7 -0
  271. package/assets/plugin/tools/get-pattern-scan.js +8 -0
  272. package/assets/plugin/tools/get-regime.d.ts +6 -0
  273. package/assets/plugin/tools/get-regime.js +7 -0
  274. package/assets/plugin/tools/get-relevant-learnings.d.ts +21 -0
  275. package/assets/plugin/tools/get-relevant-learnings.js +65 -0
  276. package/assets/plugin/tools/get-resting-liquidity.d.ts +6 -0
  277. package/assets/plugin/tools/get-resting-liquidity.js +11 -0
  278. package/assets/plugin/tools/get-risk-scenario.d.ts +29 -0
  279. package/assets/plugin/tools/get-risk-scenario.js +47 -0
  280. package/assets/plugin/tools/get-risk-summary.d.ts +51 -0
  281. package/assets/plugin/tools/get-risk-summary.js +118 -0
  282. package/assets/plugin/tools/get-sentiment.d.ts +4 -0
  283. package/assets/plugin/tools/get-sentiment.js +6 -0
  284. package/assets/plugin/tools/get-session-review.d.ts +7 -0
  285. package/assets/plugin/tools/get-session-review.js +8 -0
  286. package/assets/plugin/tools/get-setup-detail.d.ts +7 -0
  287. package/assets/plugin/tools/get-setup-detail.js +322 -0
  288. package/assets/plugin/tools/get-signals.d.ts +15 -0
  289. package/assets/plugin/tools/get-signals.js +54 -0
  290. package/assets/plugin/tools/get-sizing.d.ts +6 -0
  291. package/assets/plugin/tools/get-sizing.js +6 -0
  292. package/assets/plugin/tools/get-trade-feedback.d.ts +7 -0
  293. package/assets/plugin/tools/get-trade-feedback.js +8 -0
  294. package/assets/plugin/tools/get-trade-flow.d.ts +7 -0
  295. package/assets/plugin/tools/get-trade-flow.js +7 -0
  296. package/assets/plugin/tools/get-volume-analysis.d.ts +21 -0
  297. package/assets/plugin/tools/get-volume-analysis.js +74 -0
  298. package/assets/plugin/tools/get-volume-profile.d.ts +7 -0
  299. package/assets/plugin/tools/get-volume-profile.js +7 -0
  300. package/assets/plugin/tools/get-wave9-status.d.ts +127 -0
  301. package/assets/plugin/tools/get-wave9-status.js +796 -0
  302. package/assets/plugin/tools/helpers.d.ts +26 -0
  303. package/assets/plugin/tools/helpers.js +39 -0
  304. package/assets/plugin/tools/intel-api.d.ts +34 -0
  305. package/assets/plugin/tools/intel-api.js +119 -0
  306. package/assets/plugin/tools/intel-cache.d.ts +25 -0
  307. package/assets/plugin/tools/intel-cache.js +148 -0
  308. package/assets/plugin/tools/list-strategies.d.ts +17 -0
  309. package/assets/plugin/tools/list-strategies.js +23 -0
  310. package/assets/plugin/tools/modify-stop.d.ts +21 -0
  311. package/assets/plugin/tools/modify-stop.js +120 -0
  312. package/assets/plugin/tools/modify-target.d.ts +21 -0
  313. package/assets/plugin/tools/modify-target.js +110 -0
  314. package/assets/plugin/tools/propose-learning.d.ts +22 -0
  315. package/assets/plugin/tools/propose-learning.js +65 -0
  316. package/assets/plugin/tools/query-review-outcomes.d.ts +30 -0
  317. package/assets/plugin/tools/query-review-outcomes.js +64 -0
  318. package/assets/plugin/tools/query-trades.d.ts +21 -0
  319. package/assets/plugin/tools/query-trades.js +37 -0
  320. package/assets/plugin/tools/record-position-reviews.d.ts +38 -0
  321. package/assets/plugin/tools/record-position-reviews.js +147 -0
  322. package/assets/plugin/tools/save-strategy.d.ts +16 -0
  323. package/assets/plugin/tools/save-strategy.js +46 -0
  324. package/assets/plugin/tools/scan-pairs.d.ts +22 -0
  325. package/assets/plugin/tools/scan-pairs.js +234 -0
  326. package/assets/plugin/tools/score-setup.d.ts +31 -0
  327. package/assets/plugin/tools/score-setup.js +268 -0
  328. package/assets/plugin/tools/set-bracket-requirement.d.ts +18 -0
  329. package/assets/plugin/tools/set-bracket-requirement.js +81 -0
  330. package/assets/plugin/tools/set-exchange-credentials.d.ts +25 -0
  331. package/assets/plugin/tools/set-exchange-credentials.js +80 -0
  332. package/assets/plugin/tools/set-trading-mode.d.ts +26 -0
  333. package/assets/plugin/tools/set-trading-mode.js +152 -0
  334. package/assets/plugin/tools/test-exchange-credentials.d.ts +16 -0
  335. package/assets/plugin/tools/test-exchange-credentials.js +100 -0
  336. package/assets/plugin/tools/toggle-strategy.d.ts +8 -0
  337. package/assets/plugin/tools/toggle-strategy.js +15 -0
  338. package/assets/plugin/trading-params-cache.d.ts +26 -0
  339. package/assets/plugin/trading-params-cache.js +52 -0
  340. package/assets/plugin/types.d.ts +134 -0
  341. package/assets/plugin/types.js +7 -0
  342. package/assets/plugin/util/plugin-paths.d.ts +3 -0
  343. package/assets/plugin/util/plugin-paths.js +15 -0
  344. package/assets/plugin/venues/hyperliquid/hl-balance.d.ts +116 -0
  345. package/assets/plugin/venues/hyperliquid/hl-balance.js +145 -0
  346. package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
  347. package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
  348. package/assets/plugin/venues/hyperliquid/hl-brackets.d.ts +102 -0
  349. package/assets/plugin/venues/hyperliquid/hl-brackets.js +172 -0
  350. package/assets/plugin/venues/hyperliquid/hl-cloid.d.ts +22 -0
  351. package/assets/plugin/venues/hyperliquid/hl-cloid.js +82 -0
  352. package/assets/plugin/venues/hyperliquid/hl-info-cache.d.ts +46 -0
  353. package/assets/plugin/venues/hyperliquid/hl-info-cache.js +125 -0
  354. package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
  355. package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +728 -0
  356. package/assets/plugin/venues/hyperliquid/hl-precision.d.ts +61 -0
  357. package/assets/plugin/venues/hyperliquid/hl-precision.js +176 -0
  358. package/assets/plugin/venues/hyperliquid/hl-private.d.ts +88 -0
  359. package/assets/plugin/venues/hyperliquid/hl-private.js +357 -0
  360. package/assets/plugin/venues/hyperliquid/hl-public.d.ts +79 -0
  361. package/assets/plugin/venues/hyperliquid/hl-public.js +436 -0
  362. package/assets/plugin/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
  363. package/assets/plugin/venues/hyperliquid/hl-rate-gate.js +220 -0
  364. package/assets/plugin/venues/hyperliquid/hl-user-stream.d.ts +90 -0
  365. package/assets/plugin/venues/hyperliquid/hl-user-stream.js +220 -0
  366. package/assets/plugin/venues/registry.d.ts +38 -0
  367. package/assets/plugin/venues/registry.js +46 -0
  368. package/assets/plugin/venues/symbols.d.ts +43 -0
  369. package/assets/plugin/venues/symbols.js +107 -0
  370. package/assets/plugin/wave9/live-account-capture.d.ts +67 -0
  371. package/assets/plugin/wave9/live-account-capture.js +435 -0
  372. package/assets/plugin/wave9/live-autonomous-protection.d.ts +39 -0
  373. package/assets/plugin/wave9/live-autonomous-protection.js +112 -0
  374. package/assets/plugin/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
  375. package/assets/plugin/wave9/live-durable-reconciliation-scheduler.js +115 -0
  376. package/assets/plugin/wave9/live-execution-ledger.d.ts +107 -0
  377. package/assets/plugin/wave9/live-execution-ledger.js +498 -0
  378. package/assets/plugin/wave9/live-position-confirmation.d.ts +18 -0
  379. package/assets/plugin/wave9/live-position-confirmation.js +111 -0
  380. package/assets/plugin/wave9/live-residual-protection.d.ts +18 -0
  381. package/assets/plugin/wave9/live-residual-protection.js +250 -0
  382. package/assets/plugin/wave9/live-startup-reconciliation.d.ts +38 -0
  383. package/assets/plugin/wave9/live-startup-reconciliation.js +454 -0
  384. package/assets/plugin/wave9/live-symbol-ownership.d.ts +20 -0
  385. package/assets/plugin/wave9/live-symbol-ownership.js +132 -0
  386. package/assets/plugin/wave9/paper-admission-guard.d.ts +199 -0
  387. package/assets/plugin/wave9/paper-admission-guard.js +650 -0
  388. package/assets/plugin/wave9/usdm-evidence-provider.d.ts +42 -0
  389. package/assets/plugin/wave9/usdm-evidence-provider.js +133 -0
  390. package/dist/plugin.js +24 -15
  391. package/package.json +1 -1
@@ -0,0 +1,1333 @@
1
+ // ⚠️ GENERATED FILE — DO NOT EDIT.
2
+ // Canonical source of truth: shared/src/signals/conditions/registry.ts
3
+ // Regenerate: node scripts/sync-shared-code.mjs (enforced by shared-code-sync.test.ts)
4
+ //
5
+ // This copy exists because this package builds with tsc and deploys as a
6
+ // self-contained tree that strips workspace deps, so it cannot import
7
+ // @reefclaw/shared runtime code across the deploy boundary.
8
+ // Condition registry — all reusable condition functions for declarative strategies.
9
+ // Each condition is a pure function: (ctx, params, direction, condCtx) => ConditionResult.
10
+ import { computeEMA, computeRSI, linearSlope, findSwingPoints, mean } from '../../shared/indicators.js';
11
+ import { computeMACD, computeBollingerBands, computeVWAP, computeStochRSI, computeADX, computeIchimoku, computeSupertrend, } from '../../shared/indicators-extended.js';
12
+ // ─── Condition Registry ────────────────────────────────────────────────
13
+ const REGISTRY = new Map();
14
+ function register(type, fn) {
15
+ REGISTRY.set(type, fn);
16
+ }
17
+ export function getConditionFn(type) {
18
+ return REGISTRY.get(type);
19
+ }
20
+ export function hasCondition(type) {
21
+ return REGISTRY.has(type);
22
+ }
23
+ export function listConditionTypes() {
24
+ return Array.from(REGISTRY.keys());
25
+ }
26
+ /** Evaluate an array of condition configs. Returns SignalCondition[] + shared context. */
27
+ export function evaluateConditions(configs, ctx, direction) {
28
+ const condCtx = {};
29
+ const conditions = configs.map(cfg => {
30
+ const fn = REGISTRY.get(cfg.type);
31
+ if (!fn) {
32
+ return { name: cfg.type, met: false, currentValue: 0, threshold: 0, description: `Unknown condition: ${cfg.type}` };
33
+ }
34
+ const result = fn(ctx, cfg.params, direction, condCtx);
35
+ return { name: cfg.type, met: result.met, currentValue: result.value, threshold: result.threshold, description: result.description };
36
+ });
37
+ return { conditions, condCtx };
38
+ }
39
+ // ─── 1. ema_proximity ──────────────────────────────────────────────────
40
+ // Price within X% of EMA(N)
41
+ register('ema_proximity', (ctx, params, _dir, condCtx) => {
42
+ const period = params.period ?? 21;
43
+ const maxDistPct = params.maxDistPct ?? 0.003;
44
+ const closes = ctx.ohlcv1h.map(b => b.close);
45
+ const ema = computeEMA(closes, period);
46
+ const dist = Math.abs(ctx.currentPrice - ema) / ema;
47
+ const met = dist <= maxDistPct;
48
+ // Store EMA for entry rule
49
+ condCtx.ema21 = period === 21 ? ema : condCtx.ema21;
50
+ return {
51
+ met,
52
+ value: Math.round(dist * 10000) / 100,
53
+ threshold: maxDistPct * 100,
54
+ description: met
55
+ ? `Price within ${(dist * 100).toFixed(2)}% of EMA(${period})`
56
+ : `Price ${(dist * 100).toFixed(2)}% from EMA(${period}) (need ≤${maxDistPct * 100}%)`,
57
+ };
58
+ });
59
+ // ─── 2. orderbook_imbalance ────────────────────────────────────────────
60
+ // OB imbalance favors direction; auto-passes in backtest mode
61
+ register('orderbook_imbalance', (ctx, params, direction) => {
62
+ const threshold = params.threshold ?? 0.6;
63
+ if (ctx.backtestMode) {
64
+ return { met: true, value: 50, threshold: threshold * 100, description: 'Skipped (no historical orderbook)' };
65
+ }
66
+ const met = direction === 'LONG'
67
+ ? ctx.obImbalance > threshold
68
+ : direction === 'SHORT'
69
+ ? ctx.obImbalance < 1 - threshold
70
+ : Math.abs(ctx.obImbalance - 0.5) > (threshold - 0.5);
71
+ return {
72
+ met,
73
+ value: Math.round(ctx.obImbalance * 100),
74
+ threshold: threshold * 100,
75
+ description: met
76
+ ? `Orderbook ${direction === 'LONG' ? 'bid' : 'ask'} imbalance ${(ctx.obImbalance * 100).toFixed(0)}%`
77
+ : `Orderbook imbalance ${(ctx.obImbalance * 100).toFixed(0)}% (need ${direction === 'LONG' ? '>' : '<'}${threshold * 100}%)`,
78
+ };
79
+ });
80
+ // ─── 3. funding_not_crowded ────────────────────────────────────────────
81
+ // Funding z-score within bounds (not overcrowded)
82
+ register('funding_not_crowded', (ctx, params) => {
83
+ const maxZScore = params.maxZScore ?? 2;
84
+ const zScore = ctx.fundingStd > 0
85
+ ? Math.abs((ctx.fundingRates[0] ?? 0) - ctx.fundingMean) / ctx.fundingStd
86
+ : 0;
87
+ const met = zScore < maxZScore;
88
+ return {
89
+ met,
90
+ value: Math.round(zScore * 100) / 100,
91
+ threshold: maxZScore,
92
+ description: met
93
+ ? `Funding z-score ${zScore.toFixed(2)} (not crowded)`
94
+ : `Funding z-score ${zScore.toFixed(2)} (crowded, need <${maxZScore}σ)`,
95
+ };
96
+ });
97
+ // ─── 4. oi_slope ───────────────────────────────────────────────────────
98
+ // OI slope over N slots is >= minSlope (steady or rising)
99
+ register('oi_slope', (ctx, params) => {
100
+ const lookbackSlots = params.lookbackSlots ?? 16;
101
+ const minSlope = params.minSlope ?? 0;
102
+ const oiRecent = ctx.oiValues.slice(0, lookbackSlots).reverse();
103
+ const slope = oiRecent.length >= 4 ? linearSlope(oiRecent) : 0;
104
+ const met = slope >= minSlope;
105
+ return {
106
+ met,
107
+ value: Math.round(slope * 100) / 100,
108
+ threshold: minSlope,
109
+ description: met
110
+ ? `OI slope ${slope >= 0 ? '+' : ''}${slope.toFixed(2)} (steady/rising)`
111
+ : `OI slope ${slope.toFixed(2)} (declining)`,
112
+ };
113
+ });
114
+ // ─── 5. no_liquidation_cluster ─────────────────────────────────────────
115
+ // No major liq cluster near entry; auto-passes in backtest mode
116
+ register('no_liquidation_cluster', (ctx, params) => {
117
+ const maxUsd = params.maxUsd ?? 500_000;
118
+ if (ctx.backtestMode) {
119
+ return { met: true, value: 0, threshold: maxUsd, description: 'Skipped (no historical liquidation data)' };
120
+ }
121
+ const met = ctx.liqNearEntry < maxUsd;
122
+ return {
123
+ met,
124
+ value: Math.round(ctx.liqNearEntry),
125
+ threshold: maxUsd,
126
+ description: met
127
+ ? `Liquidations near entry $${(ctx.liqNearEntry / 1000).toFixed(0)}k (below threshold)`
128
+ : `Liquidation cluster $${(ctx.liqNearEntry / 1000).toFixed(0)}k near entry (threshold $${(maxUsd / 1000).toFixed(0)}k)`,
129
+ };
130
+ });
131
+ // ─── 6. price_sweep ────────────────────────────────────────────────────
132
+ // Price swept a swing high/low on 5m, then reversed
133
+ register('price_sweep', (ctx, params, _dir, condCtx) => {
134
+ const swingLookback = params.swingLookback ?? 5;
135
+ const recentBarCount = params.recentBars ?? 10;
136
+ const bars5m = ctx.ohlcv5m;
137
+ if (bars5m.length < 50) {
138
+ return { met: false, value: 0, threshold: 0, description: 'Insufficient 5m data' };
139
+ }
140
+ const lookbackBars = bars5m.slice(0, -recentBarCount);
141
+ const recentBars = bars5m.slice(-recentBarCount);
142
+ const swings = findSwingPoints(lookbackBars, swingLookback);
143
+ const latestClose = recentBars[recentBars.length - 1].close;
144
+ const recentLow = Math.min(...recentBars.map(b => b.low));
145
+ const recentHigh = Math.max(...recentBars.map(b => b.high));
146
+ let sweptLow = false;
147
+ let sweptHigh = false;
148
+ let sweepLevel = 0;
149
+ if (swings.lows.length > 0) {
150
+ const nearestLow = Math.min(...swings.lows.slice(-5));
151
+ if (recentLow < nearestLow && latestClose > nearestLow) {
152
+ sweptLow = true;
153
+ sweepLevel = nearestLow;
154
+ }
155
+ }
156
+ if (!sweptLow && swings.highs.length > 0) {
157
+ const nearestHigh = Math.max(...swings.highs.slice(-5));
158
+ if (recentHigh > nearestHigh && latestClose < nearestHigh) {
159
+ sweptHigh = true;
160
+ sweepLevel = nearestHigh;
161
+ }
162
+ }
163
+ // Store in shared context for entry/stop rules and direction.
164
+ // sweptDirection is written non-destructively, like the funding_extreme /
165
+ // cvd_divergence setters: only when a sweep actually resolved a side. An
166
+ // unconditional write (including null on a no-sweep bar) clobbered a
167
+ // direction set by an earlier condition in the same evaluation, so a
168
+ // from_sweep/from_funding strategy listing price_sweep AFTER another
169
+ // setter could never fire.
170
+ condCtx.sweepLevel = sweepLevel;
171
+ condCtx.recentLow = recentLow;
172
+ condCtx.recentHigh = recentHigh;
173
+ if (sweptLow)
174
+ condCtx.sweptDirection = 'LONG';
175
+ else if (sweptHigh)
176
+ condCtx.sweptDirection = 'SHORT';
177
+ const met = sweptLow || sweptHigh;
178
+ return {
179
+ met,
180
+ value: sweepLevel,
181
+ threshold: 0,
182
+ description: sweptLow
183
+ ? `Swept swing low at ${sweepLevel.toFixed(2)}, reversed above`
184
+ : sweptHigh
185
+ ? `Swept swing high at ${sweepLevel.toFixed(2)}, reversed below`
186
+ : 'No sweep detected',
187
+ };
188
+ });
189
+ // ─── 7. liquidations_at_sweep ──────────────────────────────────────────
190
+ // Liquidation volume at sweep level above minimum; auto-passes in backtest
191
+ register('liquidations_at_sweep', (ctx, params) => {
192
+ const minUsd = params.minUsd ?? 200_000;
193
+ if (ctx.backtestMode) {
194
+ return { met: true, value: 0, threshold: minUsd, description: 'Skipped (no historical liquidation data)' };
195
+ }
196
+ const liq = ctx.liqNearEntry;
197
+ const met = liq > minUsd;
198
+ return {
199
+ met,
200
+ value: Math.round(liq),
201
+ threshold: minUsd,
202
+ description: met
203
+ ? `$${(liq / 1000).toFixed(0)}k liquidations at sweep level`
204
+ : `Only $${(liq / 1000).toFixed(0)}k liquidations (need >${(minUsd / 1000).toFixed(0)}k)`,
205
+ };
206
+ });
207
+ // ─── 8. order_flow_absorption ──────────────────────────────────────────
208
+ // High volume but small net delta = absorption
209
+ register('order_flow_absorption', (ctx, params) => {
210
+ const maxRatio = params.maxRatio ?? 0.3;
211
+ const totalBuy = ctx.tradeFlow.reduce((s, b) => s + b.buyVolume, 0);
212
+ const totalSell = ctx.tradeFlow.reduce((s, b) => s + b.sellVolume, 0);
213
+ const totalVolume = totalBuy + totalSell;
214
+ const netDelta = Math.abs(totalBuy - totalSell);
215
+ const ratio = totalVolume > 0 ? netDelta / totalVolume : 1;
216
+ const met = ratio < maxRatio;
217
+ return {
218
+ met,
219
+ value: Math.round(ratio * 100),
220
+ threshold: maxRatio * 100,
221
+ description: met
222
+ ? `Absorption detected: delta/volume ratio ${(ratio * 100).toFixed(0)}%`
223
+ : `No absorption: delta/volume ratio ${(ratio * 100).toFixed(0)}% (need <${(maxRatio * 100).toFixed(0)}%)`,
224
+ };
225
+ });
226
+ // ─── 9. funding_contrarian ─────────────────────────────────────────────
227
+ // Funding not extreme in the direction of trade (direction-aware)
228
+ register('funding_contrarian', (ctx, params, direction) => {
229
+ const maxZScore = params.maxZScore ?? 1.5;
230
+ const currentFunding = ctx.fundingRates[0] ?? 0;
231
+ const zScore = ctx.fundingStd > 0 ? (currentFunding - ctx.fundingMean) / ctx.fundingStd : 0;
232
+ const met = direction === 'LONG'
233
+ ? zScore > -maxZScore
234
+ : direction === 'SHORT'
235
+ ? zScore < maxZScore
236
+ : Math.abs(zScore) < maxZScore;
237
+ return {
238
+ met,
239
+ value: Math.round(zScore * 100) / 100,
240
+ threshold: maxZScore,
241
+ description: met
242
+ ? `Funding z-score ${zScore.toFixed(2)} (neutral/contrarian)`
243
+ : `Funding z-score ${zScore.toFixed(2)} (aligned with crowd)`,
244
+ };
245
+ });
246
+ // ─── 10. funding_extreme ───────────────────────────────────────────────
247
+ // Funding > Nσ or < -Nσ from mean (for mean reversion setups)
248
+ // Also determines direction: extreme positive → SHORT, extreme negative → LONG
249
+ register('funding_extreme', (ctx, params, _dir, condCtx) => {
250
+ const minZScore = params.minZScore ?? 3;
251
+ const currentFunding = ctx.fundingRates[0] ?? 0;
252
+ const zScore = ctx.fundingStd > 0 ? (currentFunding - ctx.fundingMean) / ctx.fundingStd : 0;
253
+ const isExtremePositive = zScore > minZScore;
254
+ const isExtremeNegative = zScore < -minZScore;
255
+ const met = isExtremePositive || isExtremeNegative;
256
+ // Store direction for from_funding rule
257
+ if (isExtremePositive)
258
+ condCtx.sweptDirection = 'SHORT';
259
+ else if (isExtremeNegative)
260
+ condCtx.sweptDirection = 'LONG';
261
+ return {
262
+ met,
263
+ value: Math.round(zScore * 100) / 100,
264
+ threshold: minZScore,
265
+ description: met
266
+ ? `Funding z-score ${zScore.toFixed(2)} (extreme ${isExtremePositive ? 'positive' : 'negative'})`
267
+ : `Funding z-score ${zScore.toFixed(2)} (need >${minZScore}σ or <-${minZScore}σ)`,
268
+ };
269
+ });
270
+ // ─── 11. oi_elevated ───────────────────────────────────────────────────
271
+ // OI above N× its average (elevated positioning)
272
+ register('oi_elevated', (ctx, params) => {
273
+ const elevationRatio = params.elevationRatio ?? 1.1;
274
+ const oiCurrent = ctx.oiValues[0] ?? 0;
275
+ const oiAvg = ctx.oiValues.length > 10 ? mean(ctx.oiValues) : oiCurrent;
276
+ const ratio = oiAvg > 0 ? oiCurrent / oiAvg : 1;
277
+ const met = ratio > elevationRatio;
278
+ return {
279
+ met,
280
+ value: Math.round(ratio * 100) / 100,
281
+ threshold: elevationRatio,
282
+ description: met
283
+ ? `OI ${((ratio - 1) * 100).toFixed(0)}% above average (crowded positioning)`
284
+ : `OI only ${((ratio - 1) * 100).toFixed(0)}% vs average (need >${((elevationRatio - 1) * 100).toFixed(0)}%)`,
285
+ };
286
+ });
287
+ // ─── 12. price_at_level ────────────────────────────────────────────────
288
+ // Price near a technically significant level (EMA50, EMA200, or swing)
289
+ register('price_at_level', (ctx, params) => {
290
+ const maxDistPct = params.maxDistPct ?? 0.005;
291
+ const closes = ctx.ohlcv1h.map(b => b.close);
292
+ const ema50 = computeEMA(closes, params.emaPeriod1 ?? 50);
293
+ const ema200 = computeEMA(closes, params.emaPeriod2 ?? 200);
294
+ const swingLookbackBars = params.swingLookbackBars ?? 48;
295
+ const swingLookback = params.swingLookback ?? 3;
296
+ const swings = findSwingPoints(ctx.ohlcv1h.slice(-swingLookbackBars), swingLookback);
297
+ const distToEma50 = Math.abs(ctx.currentPrice - ema50) / ema50;
298
+ const distToEma200 = Math.abs(ctx.currentPrice - ema200) / ema200;
299
+ const distToSwing = Math.min(...swings.highs.slice(-5).map(h => Math.abs(ctx.currentPrice - h) / ctx.currentPrice), ...swings.lows.slice(-5).map(l => Math.abs(ctx.currentPrice - l) / ctx.currentPrice), 1);
300
+ const closestDist = Math.min(distToEma50, distToEma200, distToSwing);
301
+ const met = closestDist < maxDistPct;
302
+ return {
303
+ met,
304
+ value: Math.round(closestDist * 10000) / 100,
305
+ threshold: maxDistPct * 10000 / 100,
306
+ description: met
307
+ ? `Price within ${(closestDist * 100).toFixed(2)}% of significant level`
308
+ : `Price ${(closestDist * 100).toFixed(2)}% from nearest level (need ≤${(maxDistPct * 100).toFixed(1)}%)`,
309
+ };
310
+ });
311
+ // ─── 13. macd_crossover ──────────────────────────────────────────────────
312
+ // MACD line crossed signal in trade direction
313
+ register('macd_crossover', (ctx, params, direction) => {
314
+ const closes = ctx.ohlcv1h.map(b => b.close);
315
+ const macd = computeMACD(closes, params.fast ?? 12, params.slow ?? 26, params.signal ?? 9);
316
+ const met = direction === 'LONG'
317
+ ? macd.crossover === 'bullish'
318
+ : direction === 'SHORT'
319
+ ? macd.crossover === 'bearish'
320
+ : macd.crossover !== 'none';
321
+ return {
322
+ met,
323
+ value: Math.round(macd.histogram * 100) / 100,
324
+ threshold: 0,
325
+ description: met
326
+ ? `MACD ${macd.crossover} crossover (histogram ${macd.histogram.toFixed(2)})`
327
+ : `No MACD crossover in trade direction (histogram ${macd.histogram.toFixed(2)})`,
328
+ };
329
+ });
330
+ // ─── 14. macd_divergence ─────────────────────────────────────────────────
331
+ // Price vs MACD divergence (trend weakening)
332
+ register('macd_divergence', (ctx) => {
333
+ const closes = ctx.ohlcv1h.map(b => b.close);
334
+ const macd = computeMACD(closes, 12, 26, 9);
335
+ if (closes.length < 20) {
336
+ return { met: false, value: 0, threshold: 0, description: 'Insufficient data for divergence' };
337
+ }
338
+ const recent = closes.slice(-20);
339
+ const priceUp = recent[recent.length - 1] > recent[0];
340
+ const histDown = macd.histogram < 0;
341
+ const priceDown = recent[recent.length - 1] < recent[0];
342
+ const histUp = macd.histogram > 0;
343
+ const met = (priceUp && histDown) || (priceDown && histUp);
344
+ return {
345
+ met,
346
+ value: Math.round(macd.histogram * 100) / 100,
347
+ threshold: 0,
348
+ description: met
349
+ ? `MACD divergence: price ${priceUp ? 'rising' : 'falling'} vs histogram ${histUp ? 'positive' : 'negative'}`
350
+ : 'No MACD divergence detected',
351
+ };
352
+ });
353
+ // ─── 15. bollinger_squeeze ───────────────────────────────────────────────
354
+ // Bollinger bandwidth below threshold (compression)
355
+ register('bollinger_squeeze', (ctx, params) => {
356
+ const closes = ctx.ohlcv1h.map(b => b.close);
357
+ const bb = computeBollingerBands(closes, params.period ?? 20, params.stdDev ?? 2);
358
+ const maxBandwidth = params.maxBandwidth ?? 3.0;
359
+ const met = bb.bandwidth < maxBandwidth;
360
+ return {
361
+ met,
362
+ value: Math.round(bb.bandwidth * 100) / 100,
363
+ threshold: maxBandwidth,
364
+ description: met
365
+ ? `Bollinger squeeze: bandwidth ${bb.bandwidth.toFixed(2)}% (compressed)`
366
+ : `Bollinger bandwidth ${bb.bandwidth.toFixed(2)}% (need <${maxBandwidth}%)`,
367
+ };
368
+ });
369
+ // ─── 16. bollinger_breakout ──────────────────────────────────────────────
370
+ // Price closes outside Bollinger band
371
+ register('bollinger_breakout', (ctx, params, direction) => {
372
+ const closes = ctx.ohlcv1h.map(b => b.close);
373
+ const bb = computeBollingerBands(closes, params.period ?? 20, params.stdDev ?? 2);
374
+ const met = direction === 'LONG'
375
+ ? bb.percentB > 1
376
+ : direction === 'SHORT'
377
+ ? bb.percentB < 0
378
+ : bb.percentB > 1 || bb.percentB < 0;
379
+ return {
380
+ met,
381
+ value: Math.round(bb.percentB * 100) / 100,
382
+ threshold: direction === 'SHORT' ? 0 : 1,
383
+ description: met
384
+ ? `Bollinger breakout: %B=${bb.percentB.toFixed(2)} (outside band)`
385
+ : `Price inside Bollinger bands: %B=${bb.percentB.toFixed(2)}`,
386
+ };
387
+ });
388
+ // ─── 17. vwap_position ───────────────────────────────────────────────────
389
+ // Price above/below VWAP aligns with direction
390
+ register('vwap_position', (ctx, params, direction) => {
391
+ const bars = ctx.ohlcv1h.slice(-24).map(b => ({
392
+ open: b.open, high: b.high, low: b.low, close: b.close, volume: b.volume,
393
+ }));
394
+ const vwap = computeVWAP(bars);
395
+ const above = ctx.currentPrice > vwap;
396
+ const met = direction === 'LONG' ? above : direction === 'SHORT' ? !above : true;
397
+ const distPct = vwap > 0 ? ((ctx.currentPrice - vwap) / vwap) * 100 : 0;
398
+ return {
399
+ met,
400
+ value: Math.round(distPct * 100) / 100,
401
+ threshold: 0,
402
+ description: met
403
+ ? `Price ${above ? 'above' : 'below'} VWAP (${distPct.toFixed(2)}%)`
404
+ : `Price on wrong side of VWAP for ${direction} (${distPct.toFixed(2)}%)`,
405
+ };
406
+ });
407
+ // ─── 18. stoch_rsi_extreme ───────────────────────────────────────────────
408
+ // StochRSI in extreme zone
409
+ register('stoch_rsi_extreme', (ctx, params, direction) => {
410
+ const closes = ctx.ohlcv1h.map(b => b.close);
411
+ const sr = computeStochRSI(closes, 14, 14, 3, 3);
412
+ const oversoldThreshold = params.oversold ?? 20;
413
+ const overboughtThreshold = params.overbought ?? 80;
414
+ const met = direction === 'LONG'
415
+ ? sr.k < oversoldThreshold
416
+ : direction === 'SHORT'
417
+ ? sr.k > overboughtThreshold
418
+ : sr.k < oversoldThreshold || sr.k > overboughtThreshold;
419
+ return {
420
+ met,
421
+ value: sr.k,
422
+ threshold: direction === 'SHORT' ? overboughtThreshold : oversoldThreshold,
423
+ description: met
424
+ ? `StochRSI extreme: K=${sr.k} (${sr.k < 50 ? 'oversold' : 'overbought'})`
425
+ : `StochRSI K=${sr.k} (not extreme)`,
426
+ };
427
+ });
428
+ // ─── 19. adx_trending ────────────────────────────────────────────────────
429
+ // ADX above threshold (trending market)
430
+ register('adx_trending', (ctx, params) => {
431
+ const closes = ctx.ohlcv1h.map(b => b.close);
432
+ const highs = ctx.ohlcv1h.map(b => b.high);
433
+ const lows = ctx.ohlcv1h.map(b => b.low);
434
+ const adx = computeADX(highs, lows, closes, params.period ?? 14);
435
+ const minAdx = params.minAdx ?? 25;
436
+ const met = adx.adx > minAdx;
437
+ return {
438
+ met,
439
+ value: adx.adx,
440
+ threshold: minAdx,
441
+ description: met
442
+ ? `ADX ${adx.adx} (trending, +DI=${adx.plusDI} -DI=${adx.minusDI})`
443
+ : `ADX ${adx.adx} (not trending, need >${minAdx})`,
444
+ };
445
+ });
446
+ // ─── 20. adx_ranging ─────────────────────────────────────────────────────
447
+ // ADX below threshold (ranging market)
448
+ register('adx_ranging', (ctx, params) => {
449
+ const closes = ctx.ohlcv1h.map(b => b.close);
450
+ const highs = ctx.ohlcv1h.map(b => b.high);
451
+ const lows = ctx.ohlcv1h.map(b => b.low);
452
+ const adx = computeADX(highs, lows, closes, params.period ?? 14);
453
+ const maxAdx = params.maxAdx ?? 20;
454
+ const met = adx.adx < maxAdx;
455
+ return {
456
+ met,
457
+ value: adx.adx,
458
+ threshold: maxAdx,
459
+ description: met
460
+ ? `ADX ${adx.adx} (ranging, choppy market)`
461
+ : `ADX ${adx.adx} (too strong for range, need <${maxAdx})`,
462
+ };
463
+ });
464
+ // ─── 21. ichimoku_cloud ──────────────────────────────────────────────────
465
+ // Price position relative to Ichimoku cloud + TK cross
466
+ register('ichimoku_cloud', (ctx, params, direction) => {
467
+ const closes = ctx.ohlcv1h.map(b => b.close);
468
+ const highs = ctx.ohlcv1h.map(b => b.high);
469
+ const lows = ctx.ohlcv1h.map(b => b.low);
470
+ const ichi = computeIchimoku(highs, lows, closes, 9, 26, 52);
471
+ const bullish = ichi.cloudPosition === 'above' && ichi.tenkan > ichi.kijun;
472
+ const bearish = ichi.cloudPosition === 'below' && ichi.tenkan < ichi.kijun;
473
+ const met = direction === 'LONG' ? bullish : direction === 'SHORT' ? bearish : bullish || bearish;
474
+ return {
475
+ met,
476
+ value: ichi.cloudPosition === 'above' ? 1 : ichi.cloudPosition === 'below' ? -1 : 0,
477
+ threshold: 0,
478
+ description: met
479
+ ? `Ichimoku ${bullish ? 'bullish' : 'bearish'}: ${ichi.cloudPosition} cloud, TK ${ichi.tenkan > ichi.kijun ? 'bull' : 'bear'} cross`
480
+ : `Ichimoku neutral: ${ichi.cloudPosition} cloud`,
481
+ };
482
+ });
483
+ // ─── 22. obv_divergence ──────────────────────────────────────────────────
484
+ // OBV diverges from price
485
+ register('obv_divergence', (ctx, params, direction) => {
486
+ const closes = ctx.ohlcv1h.map(b => b.close);
487
+ const volumes = ctx.ohlcv1h.map(b => b.volume);
488
+ if (closes.length < 20) {
489
+ return { met: false, value: 0, threshold: 0, description: 'Insufficient data' };
490
+ }
491
+ const recentCloses = closes.slice(-20);
492
+ const recentVolumes = volumes.slice(-20);
493
+ let obv = 0;
494
+ const obvSeries = [0];
495
+ for (let i = 1; i < recentCloses.length; i++) {
496
+ if (recentCloses[i] > recentCloses[i - 1])
497
+ obv += recentVolumes[i];
498
+ else if (recentCloses[i] < recentCloses[i - 1])
499
+ obv -= recentVolumes[i];
500
+ obvSeries.push(obv);
501
+ }
502
+ const priceSlope = linearSlope(recentCloses);
503
+ const obvSlope = linearSlope(obvSeries);
504
+ const bullishDiv = priceSlope < 0 && obvSlope > 0;
505
+ const bearishDiv = priceSlope > 0 && obvSlope < 0;
506
+ const met = direction === 'LONG' ? bullishDiv : direction === 'SHORT' ? bearishDiv : bullishDiv || bearishDiv;
507
+ return {
508
+ met,
509
+ value: Math.round(obvSlope * 100) / 100,
510
+ threshold: 0,
511
+ description: met
512
+ ? `OBV ${bullishDiv ? 'bullish' : 'bearish'} divergence (accumulation/distribution)`
513
+ : 'No OBV divergence detected',
514
+ };
515
+ });
516
+ // ─── 24. cvd_divergence_bullish ──────────────────────────────────────────
517
+ // CVD bullish divergence detected (price lower low + CVD higher low)
518
+ register('cvd_divergence_bullish', (ctx, _params, _dir, condCtx) => {
519
+ if (!ctx.cvd) {
520
+ if (ctx.backtestMode)
521
+ return { met: true, value: 0, threshold: 0, description: 'Skipped (no CVD data in backtest)' };
522
+ return { met: false, value: 0, threshold: 0, description: 'No CVD data available' };
523
+ }
524
+ const met = ctx.cvd.cvdDivergence === 'bullish';
525
+ if (met)
526
+ condCtx.sweptDirection = condCtx.sweptDirection ?? 'LONG';
527
+ return {
528
+ met,
529
+ value: Math.round(ctx.cvd.cvdDivergenceStrength * 100),
530
+ threshold: 0,
531
+ description: met
532
+ ? `CVD bullish divergence (strength ${(ctx.cvd.cvdDivergenceStrength * 100).toFixed(0)}%)`
533
+ : 'No CVD bullish divergence',
534
+ };
535
+ });
536
+ // ─── 25. cvd_divergence_bearish ─────────────────────────────────────────
537
+ // CVD bearish divergence detected (price higher high + CVD lower high)
538
+ register('cvd_divergence_bearish', (ctx, _params, _dir, condCtx) => {
539
+ if (!ctx.cvd) {
540
+ if (ctx.backtestMode)
541
+ return { met: true, value: 0, threshold: 0, description: 'Skipped (no CVD data in backtest)' };
542
+ return { met: false, value: 0, threshold: 0, description: 'No CVD data available' };
543
+ }
544
+ const met = ctx.cvd.cvdDivergence === 'bearish';
545
+ if (met)
546
+ condCtx.sweptDirection = condCtx.sweptDirection ?? 'SHORT';
547
+ return {
548
+ met,
549
+ value: Math.round(ctx.cvd.cvdDivergenceStrength * 100),
550
+ threshold: 0,
551
+ description: met
552
+ ? `CVD bearish divergence (strength ${(ctx.cvd.cvdDivergenceStrength * 100).toFixed(0)}%)`
553
+ : 'No CVD bearish divergence',
554
+ };
555
+ });
556
+ // ─── 26. cvd_positive_slope ─────────────────────────────────────────────
557
+ // CVD slope is positive (buying pressure increasing)
558
+ register('cvd_positive_slope', (ctx, params) => {
559
+ if (!ctx.cvd) {
560
+ if (ctx.backtestMode)
561
+ return { met: true, value: 0, threshold: 0, description: 'Skipped (no CVD data in backtest)' };
562
+ return { met: false, value: 0, threshold: 0, description: 'No CVD data available' };
563
+ }
564
+ const minSlope = params.minSlope ?? 0;
565
+ const met = ctx.cvd.cvdSlope1h > minSlope;
566
+ return {
567
+ met,
568
+ value: Math.round(ctx.cvd.cvdSlope1h * 100) / 100,
569
+ threshold: minSlope,
570
+ description: met
571
+ ? `CVD slope positive: ${ctx.cvd.cvdSlope1h.toFixed(2)} (buying pressure increasing)`
572
+ : `CVD slope ${ctx.cvd.cvdSlope1h.toFixed(2)} (buying pressure weak)`,
573
+ };
574
+ });
575
+ // ─── 27. cvd_negative_slope ─────────────────────────────────────────────
576
+ // CVD slope is negative (selling pressure increasing)
577
+ register('cvd_negative_slope', (ctx, params) => {
578
+ if (!ctx.cvd) {
579
+ if (ctx.backtestMode)
580
+ return { met: true, value: 0, threshold: 0, description: 'Skipped (no CVD data in backtest)' };
581
+ return { met: false, value: 0, threshold: 0, description: 'No CVD data available' };
582
+ }
583
+ const maxSlope = params.maxSlope ?? 0;
584
+ const met = ctx.cvd.cvdSlope1h < maxSlope;
585
+ return {
586
+ met,
587
+ value: Math.round(ctx.cvd.cvdSlope1h * 100) / 100,
588
+ threshold: maxSlope,
589
+ description: met
590
+ ? `CVD slope negative: ${ctx.cvd.cvdSlope1h.toFixed(2)} (selling pressure increasing)`
591
+ : `CVD slope ${ctx.cvd.cvdSlope1h.toFixed(2)} (selling pressure weak)`,
592
+ };
593
+ });
594
+ // ─── 28. cvd_extreme ────────────────────────────────────────────────────
595
+ // CVD z-score at extreme (>2σ = exhaustion signal)
596
+ register('cvd_extreme', (ctx, params) => {
597
+ if (!ctx.cvd) {
598
+ if (ctx.backtestMode)
599
+ return { met: true, value: 0, threshold: 0, description: 'Skipped (no CVD data in backtest)' };
600
+ return { met: false, value: 0, threshold: 0, description: 'No CVD data available' };
601
+ }
602
+ const minZScore = params.minZScore ?? 2;
603
+ const absZ = Math.abs(ctx.cvd.cvdZScore);
604
+ const met = absZ > minZScore;
605
+ return {
606
+ met,
607
+ value: Math.round(ctx.cvd.cvdZScore * 100) / 100,
608
+ threshold: minZScore,
609
+ description: met
610
+ ? `CVD extreme: z-score ${ctx.cvd.cvdZScore.toFixed(2)} (${ctx.cvd.cvdZScore > 0 ? 'buying' : 'selling'} exhaustion)`
611
+ : `CVD z-score ${ctx.cvd.cvdZScore.toFixed(2)} (not extreme, need >${minZScore}σ)`,
612
+ };
613
+ });
614
+ // ─── 23. supertrend_direction ────────────────────────────────────────────
615
+ // Supertrend indicator direction matches trade direction
616
+ register('supertrend_direction', (ctx, params, direction) => {
617
+ const closes = ctx.ohlcv1h.map(b => b.close);
618
+ const highs = ctx.ohlcv1h.map(b => b.high);
619
+ const lows = ctx.ohlcv1h.map(b => b.low);
620
+ const st = computeSupertrend(highs, lows, closes, params.period ?? 10, params.multiplier ?? 3);
621
+ const met = direction === 'LONG'
622
+ ? st.direction === 'bullish'
623
+ : direction === 'SHORT'
624
+ ? st.direction === 'bearish'
625
+ : true;
626
+ return {
627
+ met,
628
+ value: st.direction === 'bullish' ? 1 : -1,
629
+ threshold: 0,
630
+ description: met
631
+ ? `Supertrend ${st.direction} (value ${st.value.toFixed(2)})`
632
+ : `Supertrend ${st.direction} conflicts with ${direction} direction`,
633
+ };
634
+ });
635
+ // ─── Helpers for higher-timeframe pattern conditions (Path B) ─────────
636
+ // All conditions below honour params.tfHours: 1 (default, ohlcv1h),
637
+ // 4 (ohlcv4h), or 24 (ohlcv1d). When the requested higher timeframe is
638
+ // unavailable on this MarketContext, they fall back to ohlcv1h so unit
639
+ // tests and legacy contexts still work.
640
+ function pickBars(ctx, tfHours) {
641
+ if (tfHours >= 24)
642
+ return ctx.ohlcv1d ?? ctx.ohlcv1h;
643
+ if (tfHours >= 4)
644
+ return ctx.ohlcv4h ?? ctx.ohlcv1h;
645
+ return ctx.ohlcv1h;
646
+ }
647
+ /** Pivot finder that returns indices alongside prices. We can't reuse
648
+ * `findSwingPoints` from shared/indicators because it discards indices,
649
+ * and trendline-fit + multi-pivot divergence both need them. */
650
+ function findPivotsIdx(bars, lookback) {
651
+ const highs = [];
652
+ const lows = [];
653
+ for (let i = lookback; i < bars.length - lookback; i++) {
654
+ let isHigh = true;
655
+ let isLow = true;
656
+ for (let j = i - lookback; j <= i + lookback; j++) {
657
+ if (j === i)
658
+ continue;
659
+ if (bars[j].high >= bars[i].high)
660
+ isHigh = false;
661
+ if (bars[j].low <= bars[i].low)
662
+ isLow = false;
663
+ }
664
+ if (isHigh)
665
+ highs.push({ idx: i, price: bars[i].high });
666
+ if (isLow)
667
+ lows.push({ idx: i, price: bars[i].low });
668
+ }
669
+ return { highs, lows };
670
+ }
671
+ // ─── 30. ema_slope ───────────────────────────────────────────────────
672
+ // EMA(N) is sloping in the trade direction at >= minSlopePct per bar
673
+ // (slope normalised by current EMA value, so it's a percent change rate).
674
+ // params: period, lookback, minSlopePct, dirSign (1=up,-1=down,0=use trade direction), tfHours
675
+ register('ema_slope', (ctx, params, direction) => {
676
+ const period = Math.floor(params.period ?? 21);
677
+ const lookback = Math.floor(params.lookback ?? 5);
678
+ const minSlopePct = params.minSlopePct ?? 0;
679
+ const dirSign = params.dirSign ?? 0;
680
+ const tfHours = params.tfHours ?? 1;
681
+ const bars = pickBars(ctx, tfHours);
682
+ if (bars.length < period + lookback + 5) {
683
+ return { met: false, value: 0, threshold: minSlopePct * 100, description: `Insufficient ${tfHours}h bars for ema_slope` };
684
+ }
685
+ const closes = bars.map((b) => b.close);
686
+ const emaSeries = [];
687
+ for (let i = closes.length - lookback - 1; i < closes.length; i++) {
688
+ emaSeries.push(computeEMA(closes.slice(0, i + 1), period));
689
+ }
690
+ const slope = linearSlope(emaSeries);
691
+ const lastEma = emaSeries[emaSeries.length - 1];
692
+ const slopePct = lastEma > 0 ? slope / lastEma : 0;
693
+ const requiredSign = dirSign !== 0
694
+ ? Math.sign(dirSign)
695
+ : direction === 'LONG' ? 1 : direction === 'SHORT' ? -1 : 0;
696
+ const met = requiredSign === 0
697
+ ? Math.abs(slopePct) >= minSlopePct
698
+ : (slopePct * requiredSign) >= minSlopePct;
699
+ return {
700
+ met,
701
+ value: Math.round(slopePct * 1_000_000) / 10_000,
702
+ threshold: minSlopePct * 100,
703
+ description: met
704
+ ? `EMA(${period}) slope ${(slopePct * 100).toFixed(3)}%/bar over ${lookback} ${tfHours}h bars (${slopePct > 0 ? 'rising' : slopePct < 0 ? 'falling' : 'flat'})`
705
+ : `EMA(${period}) slope ${(slopePct * 100).toFixed(3)}%/bar fails ${requiredSign > 0 ? '≥+' : requiredSign < 0 ? '≤−' : '|·|≥'}${(minSlopePct * 100).toFixed(3)}%`,
706
+ };
707
+ });
708
+ // ─── 31. volume_vs_sma ───────────────────────────────────────────────
709
+ // Latest bar volume >= multiplier × N-bar SMA of prior volumes.
710
+ // params: period, minMultiplier, tfHours
711
+ register('volume_vs_sma', (ctx, params) => {
712
+ const period = Math.floor(params.period ?? 20);
713
+ const minMultiplier = params.minMultiplier ?? 1.5;
714
+ const tfHours = params.tfHours ?? 1;
715
+ const bars = pickBars(ctx, tfHours);
716
+ if (bars.length < period + 1) {
717
+ return { met: false, value: 0, threshold: minMultiplier, description: `Insufficient ${tfHours}h bars for volume_vs_sma` };
718
+ }
719
+ const currentVol = bars[bars.length - 1].volume;
720
+ const prevVols = bars.slice(-period - 1, -1).map((b) => b.volume);
721
+ const sma = mean(prevVols);
722
+ const ratio = sma > 0 ? currentVol / sma : 0;
723
+ const met = ratio >= minMultiplier;
724
+ return {
725
+ met,
726
+ value: Math.round(ratio * 100) / 100,
727
+ threshold: minMultiplier,
728
+ description: met
729
+ ? `Volume ${ratio.toFixed(2)}× ${period}-bar SMA on ${tfHours}h`
730
+ : `Volume only ${ratio.toFixed(2)}× ${period}-bar SMA (need ≥${minMultiplier}×)`,
731
+ };
732
+ });
733
+ // Direction-aware return momentum over a fixed lookback.
734
+ //
735
+ // params:
736
+ // lookback: bars back from the latest close
737
+ // minReturnPct: decimal return threshold, e.g. 0.02 = 2%
738
+ // maxAbsReturnPct: optional decimal cap to avoid chasing blow-off moves
739
+ // mode: 1 = momentum with direction, -1 = counter-move into direction,
740
+ // 0 = absolute move either way
741
+ // dirSign: 1 long/up, -1 short/down, 0 use resolved trade direction
742
+ // tfHours: 1, 4, or 24
743
+ register('return_momentum', (ctx, params, direction) => {
744
+ const lookback = Math.max(1, Math.floor(params.lookback ?? 12));
745
+ const minReturnPct = params.minReturnPct ?? 0.01;
746
+ const maxAbsReturnPct = params.maxAbsReturnPct ?? 0;
747
+ const mode = params.mode === 0 ? 0 : params.mode && params.mode < 0 ? -1 : 1;
748
+ const dirSign = params.dirSign ?? 0;
749
+ const tfHours = params.tfHours ?? 1;
750
+ const bars = pickBars(ctx, tfHours);
751
+ if (bars.length <= lookback) {
752
+ return {
753
+ met: false,
754
+ value: 0,
755
+ threshold: minReturnPct * 100,
756
+ description: `Insufficient ${tfHours}h bars for return_momentum`,
757
+ };
758
+ }
759
+ const currentClose = bars[bars.length - 1].close;
760
+ const priorClose = bars[bars.length - 1 - lookback].close;
761
+ const ret = priorClose > 0 ? (currentClose - priorClose) / priorClose : 0;
762
+ const absRet = Math.abs(ret);
763
+ const baseSign = dirSign !== 0
764
+ ? Math.sign(dirSign)
765
+ : direction === 'LONG' ? 1 : direction === 'SHORT' ? -1 : 0;
766
+ const targetSign = mode === 0 || baseSign === 0 ? 0 : baseSign * mode;
767
+ const directionOk = targetSign > 0
768
+ ? ret >= minReturnPct
769
+ : targetSign < 0
770
+ ? ret <= -minReturnPct
771
+ : absRet >= minReturnPct;
772
+ const extensionOk = maxAbsReturnPct <= 0 || absRet <= maxAbsReturnPct;
773
+ const met = directionOk && extensionOk;
774
+ const label = targetSign > 0
775
+ ? 'upside momentum'
776
+ : targetSign < 0
777
+ ? 'downside momentum'
778
+ : 'absolute momentum';
779
+ const capText = maxAbsReturnPct > 0 ? `, cap ${(maxAbsReturnPct * 100).toFixed(1)}%` : '';
780
+ return {
781
+ met,
782
+ value: Math.round(ret * 10000) / 100,
783
+ threshold: minReturnPct * 100,
784
+ description: met
785
+ ? `${lookback}-bar ${tfHours}h return ${(ret * 100).toFixed(2)}% confirms ${label}`
786
+ : `${lookback}-bar ${tfHours}h return ${(ret * 100).toFixed(2)}% fails ${label} threshold ${(minReturnPct * 100).toFixed(1)}%${capText}`,
787
+ };
788
+ });
789
+ // Strict return-momentum episode transition. Unlike return_momentum, this is
790
+ // an event rather than a level: it fires once when the lookback return crosses
791
+ // zero (or an explicit threshold), then remains false until the sign resets.
792
+ // That makes stop-outs and capacity-rejected entries naturally lock out for
793
+ // the rest of the same momentum episode.
794
+ register('return_momentum_zero_cross', (ctx, params, direction) => {
795
+ const lookback = Math.max(1, Math.floor(params.lookback ?? 28));
796
+ const thresholdPct = Math.max(0, params.thresholdPct ?? 0);
797
+ const dirSign = params.dirSign ?? 0;
798
+ const tfHours = params.tfHours ?? 24;
799
+ const bars = pickBars(ctx, tfHours);
800
+ if (bars.length <= lookback + 1) {
801
+ return {
802
+ met: false,
803
+ value: 0,
804
+ threshold: thresholdPct * 100,
805
+ description: `Insufficient ${tfHours}h bars for return_momentum_zero_cross`,
806
+ };
807
+ }
808
+ const currentClose = bars[bars.length - 1].close;
809
+ const currentPrior = bars[bars.length - 1 - lookback].close;
810
+ const previousClose = bars[bars.length - 2].close;
811
+ const previousPrior = bars[bars.length - 2 - lookback].close;
812
+ const currentReturn = currentPrior > 0
813
+ ? (currentClose - currentPrior) / currentPrior
814
+ : Number.NaN;
815
+ const previousReturn = previousPrior > 0
816
+ ? (previousClose - previousPrior) / previousPrior
817
+ : Number.NaN;
818
+ const sign = dirSign !== 0
819
+ ? Math.sign(dirSign)
820
+ : direction === 'LONG' ? 1 : direction === 'SHORT' ? -1 : 0;
821
+ const met = Number.isFinite(currentReturn)
822
+ && Number.isFinite(previousReturn)
823
+ && (sign > 0
824
+ ? previousReturn <= thresholdPct && currentReturn > thresholdPct
825
+ : sign < 0
826
+ ? previousReturn >= -thresholdPct && currentReturn < -thresholdPct
827
+ : false);
828
+ return {
829
+ met,
830
+ value: Number.isFinite(currentReturn)
831
+ ? Math.round(currentReturn * 10000) / 100
832
+ : 0,
833
+ threshold: thresholdPct * 100,
834
+ description: Number.isFinite(currentReturn) && Number.isFinite(previousReturn)
835
+ ? `${lookback}-bar ${tfHours}h return moved from ${(previousReturn * 100).toFixed(2)}% to ${(currentReturn * 100).toFixed(2)}%`
836
+ : `Invalid ${tfHours}h prices for return_momentum_zero_cross`,
837
+ };
838
+ });
839
+ // ─── 32. rsi_multi_pivot_divergence ──────────────────────────────────
840
+ // Strict N-pivot RSI divergence: price makes successively lower lows (or
841
+ // higher highs) while RSI makes the opposite, with optional volume dry-up
842
+ // at the final pivot.
843
+ // params: pivots, swingLookback, rsiPeriod, volumeDryUp (0=disabled,
844
+ // 0..1=ratio cap of last/first vol), direction (1=bullish, -1=bearish), tfHours
845
+ register('rsi_multi_pivot_divergence', (ctx, params) => {
846
+ const pivotsN = Math.max(2, Math.floor(params.pivots ?? 3));
847
+ const swingLookback = Math.floor(params.swingLookback ?? 5);
848
+ const rsiPeriod = Math.floor(params.rsiPeriod ?? 14);
849
+ const volumeDryUp = params.volumeDryUp ?? 0;
850
+ const dirParam = params.direction ?? 1;
851
+ const isBullish = dirParam >= 0;
852
+ const tfHours = params.tfHours ?? 1;
853
+ const bars = pickBars(ctx, tfHours);
854
+ if (bars.length < rsiPeriod + swingLookback * 2 + 10) {
855
+ return { met: false, value: 0, threshold: pivotsN, description: `Insufficient ${tfHours}h bars for rsi_multi_pivot_divergence` };
856
+ }
857
+ const found = findPivotsIdx(bars, swingLookback);
858
+ const piv = isBullish ? found.lows : found.highs;
859
+ if (piv.length < pivotsN) {
860
+ return {
861
+ met: false,
862
+ value: piv.length,
863
+ threshold: pivotsN,
864
+ description: `Only ${piv.length} pivot ${isBullish ? 'lows' : 'highs'} on ${tfHours}h (need ${pivotsN})`,
865
+ };
866
+ }
867
+ const last = piv.slice(-pivotsN);
868
+ // Price sequence check
869
+ let priceSeqOk = true;
870
+ for (let i = 1; i < last.length; i++) {
871
+ if (isBullish && !(last[i].price < last[i - 1].price))
872
+ priceSeqOk = false;
873
+ if (!isBullish && !(last[i].price > last[i - 1].price))
874
+ priceSeqOk = false;
875
+ }
876
+ if (!priceSeqOk) {
877
+ return {
878
+ met: false,
879
+ value: 0,
880
+ threshold: pivotsN,
881
+ description: `${isBullish ? 'Price lows' : 'Price highs'} not in ${isBullish ? 'descending' : 'ascending'} sequence: ${last.map((p) => p.price.toFixed(2)).join('→')}`,
882
+ };
883
+ }
884
+ // RSI at each pivot index
885
+ const closes = bars.map((b) => b.close);
886
+ const rsiAt = last.map((p) => computeRSI(closes.slice(0, p.idx + 1), rsiPeriod));
887
+ let rsiSeqOk = true;
888
+ for (let i = 1; i < rsiAt.length; i++) {
889
+ if (isBullish && !(rsiAt[i] > rsiAt[i - 1]))
890
+ rsiSeqOk = false;
891
+ if (!isBullish && !(rsiAt[i] < rsiAt[i - 1]))
892
+ rsiSeqOk = false;
893
+ }
894
+ if (!rsiSeqOk) {
895
+ return {
896
+ met: false,
897
+ value: rsiAt[rsiAt.length - 1] - rsiAt[0],
898
+ threshold: 0,
899
+ description: `RSI not diverging: ${rsiAt.map((r) => r.toFixed(0)).join('→')}`,
900
+ };
901
+ }
902
+ if (volumeDryUp > 0) {
903
+ const v0 = bars[last[0].idx].volume;
904
+ const vN = bars[last[last.length - 1].idx].volume;
905
+ const ratio = v0 > 0 ? vN / v0 : 1;
906
+ if (ratio > volumeDryUp) {
907
+ return {
908
+ met: false,
909
+ value: Math.round(ratio * 100) / 100,
910
+ threshold: volumeDryUp,
911
+ description: `No volume dry-up: last pivot vol ${(ratio * 100).toFixed(0)}% of first (need ≤${(volumeDryUp * 100).toFixed(0)}%)`,
912
+ };
913
+ }
914
+ }
915
+ return {
916
+ met: true,
917
+ value: Math.round((rsiAt[rsiAt.length - 1] - rsiAt[0]) * 100) / 100,
918
+ threshold: 0,
919
+ description: `${pivotsN}-pivot ${isBullish ? 'bullish' : 'bearish'} divergence on ${tfHours}h: price ${last.map((p) => p.price.toFixed(2)).join('→')}, RSI ${rsiAt.map((r) => r.toFixed(0)).join('→')}`,
920
+ };
921
+ });
922
+ // ─── 33. trendline_break ─────────────────────────────────────────────
923
+ // Linear-fit a trendline through the last N pivot lows (or highs) and
924
+ // check whether the latest close has broken through it in the requested
925
+ // direction. Slope sign is also constrained to match the expected pattern
926
+ // (descending trendline for bullish break above, ascending for bearish
927
+ // break below).
928
+ // params: pivots, swingLookback, useHighs (0=lows, 1=highs), breakDir
929
+ // (1=above, -1=below), requireSlopeSign (1=ascending, -1=descending,
930
+ // default flips with useHighs), tfHours
931
+ register('trendline_break', (ctx, params) => {
932
+ const pivotsN = Math.max(2, Math.floor(params.pivots ?? 3));
933
+ const swingLookback = Math.floor(params.swingLookback ?? 5);
934
+ const useHighs = (params.useHighs ?? 0) > 0;
935
+ const breakDir = (params.breakDir ?? 1) >= 0 ? 1 : -1;
936
+ const defaultSlope = useHighs ? 1 : -1;
937
+ const requireSlope = (params.requireSlopeSign ?? defaultSlope) >= 0 ? 1 : -1;
938
+ const tfHours = params.tfHours ?? 1;
939
+ const bars = pickBars(ctx, tfHours);
940
+ if (bars.length < swingLookback * 2 + pivotsN + 5) {
941
+ return { met: false, value: 0, threshold: 0, description: `Insufficient ${tfHours}h bars for trendline_break` };
942
+ }
943
+ const found = findPivotsIdx(bars, swingLookback);
944
+ const piv = useHighs ? found.highs : found.lows;
945
+ if (piv.length < pivotsN) {
946
+ return {
947
+ met: false,
948
+ value: piv.length,
949
+ threshold: pivotsN,
950
+ description: `Only ${piv.length} pivot ${useHighs ? 'highs' : 'lows'} on ${tfHours}h (need ${pivotsN})`,
951
+ };
952
+ }
953
+ const lastPivots = piv.slice(-pivotsN);
954
+ const xs = lastPivots.map((p) => p.idx);
955
+ const ys = lastPivots.map((p) => p.price);
956
+ const slope = linearSlope(ys, xs);
957
+ const intercept = mean(ys) - slope * mean(xs);
958
+ if ((requireSlope > 0 && slope <= 0) || (requireSlope < 0 && slope >= 0)) {
959
+ return {
960
+ met: false,
961
+ value: Math.round(slope * 10000) / 10000,
962
+ threshold: 0,
963
+ description: `Trendline slope ${slope.toExponential(2)} wrong sign (need ${requireSlope > 0 ? '>0' : '<0'})`,
964
+ };
965
+ }
966
+ const lastIdx = bars.length - 1;
967
+ const lastClose = bars[lastIdx].close;
968
+ const lineAtLast = slope * lastIdx + intercept;
969
+ const broken = breakDir > 0 ? lastClose > lineAtLast : lastClose < lineAtLast;
970
+ return {
971
+ met: broken,
972
+ value: Math.round((lastClose - lineAtLast) * 100) / 100,
973
+ threshold: 0,
974
+ description: broken
975
+ ? `${pivotsN}-pivot ${useHighs ? 'highs' : 'lows'} trendline broken ${breakDir > 0 ? 'above' : 'below'}: close ${lastClose.toFixed(2)} vs line ${lineAtLast.toFixed(2)}`
976
+ : `Close ${lastClose.toFixed(2)} ${breakDir > 0 ? 'still below' : 'still above'} trendline ${lineAtLast.toFixed(2)}`,
977
+ };
978
+ });
979
+ // ─── 34. wedge_breakout ──────────────────────────────────────────────
980
+ // Detect a falling wedge in the last N bars and confirm a breakout above
981
+ // the upper trendline with volume confirmation. Falling wedge = both
982
+ // trendlines sloping down with the upper line falling faster (converging).
983
+ // params: minBars, swingLookback, minVolMultiplier, tfHours
984
+ // (Currently implements falling wedge only — the bullish-break variant
985
+ // the operator's strategies need. Rising wedge can be added later.)
986
+ register('wedge_breakout', (ctx, params) => {
987
+ const minBars = Math.floor(params.minBars ?? 40);
988
+ const swingLookback = Math.floor(params.swingLookback ?? 5);
989
+ const minVolMultiplier = params.minVolMultiplier ?? 1.5;
990
+ const tfHours = params.tfHours ?? 1;
991
+ const bars = pickBars(ctx, tfHours);
992
+ if (bars.length < minBars + 5) {
993
+ return { met: false, value: 0, threshold: 0, description: `Insufficient ${tfHours}h bars for wedge_breakout` };
994
+ }
995
+ const window = bars.slice(-minBars);
996
+ const found = findPivotsIdx(window, swingLookback);
997
+ if (found.highs.length < 3 || found.lows.length < 3) {
998
+ return {
999
+ met: false,
1000
+ value: 0,
1001
+ threshold: 0,
1002
+ description: `Need ≥3 highs+lows in ${minBars}-bar ${tfHours}h window (have ${found.highs.length}/${found.lows.length})`,
1003
+ };
1004
+ }
1005
+ const xsH = found.highs.map((h) => h.idx);
1006
+ const ysH = found.highs.map((h) => h.price);
1007
+ const xsL = found.lows.map((l) => l.idx);
1008
+ const ysL = found.lows.map((l) => l.price);
1009
+ const slopeH = linearSlope(ysH, xsH);
1010
+ const slopeL = linearSlope(ysL, xsL);
1011
+ const interceptH = mean(ysH) - slopeH * mean(xsH);
1012
+ const isFallingWedge = slopeH < 0 && slopeL < 0 && Math.abs(slopeH) > Math.abs(slopeL);
1013
+ if (!isFallingWedge) {
1014
+ return {
1015
+ met: false,
1016
+ value: 0,
1017
+ threshold: 0,
1018
+ description: `Not a falling wedge: upper slope ${slopeH.toExponential(2)}, lower ${slopeL.toExponential(2)}`,
1019
+ };
1020
+ }
1021
+ const lastIdx = window.length - 1;
1022
+ const upperAtLast = slopeH * lastIdx + interceptH;
1023
+ const lastClose = window[lastIdx].close;
1024
+ if (lastClose <= upperAtLast) {
1025
+ return {
1026
+ met: false,
1027
+ value: Math.round((lastClose - upperAtLast) * 100) / 100,
1028
+ threshold: 0,
1029
+ description: `Wedge intact: close ${lastClose.toFixed(2)} ≤ upper line ${upperAtLast.toFixed(2)}`,
1030
+ };
1031
+ }
1032
+ const lastVol = window[lastIdx].volume;
1033
+ const volSma = mean(window.slice(-21, -1).map((b) => b.volume));
1034
+ const volRatio = volSma > 0 ? lastVol / volSma : 0;
1035
+ if (volRatio < minVolMultiplier) {
1036
+ return {
1037
+ met: false,
1038
+ value: Math.round(volRatio * 100) / 100,
1039
+ threshold: minVolMultiplier,
1040
+ description: `Wedge break but volume ${volRatio.toFixed(2)}× SMA (need ≥${minVolMultiplier}×)`,
1041
+ };
1042
+ }
1043
+ return {
1044
+ met: true,
1045
+ value: Math.round((lastClose - upperAtLast) * 100) / 100,
1046
+ threshold: 0,
1047
+ description: `Falling wedge break on ${tfHours}h: close ${lastClose.toFixed(2)} > upper ${upperAtLast.toFixed(2)}, vol ${volRatio.toFixed(1)}× SMA`,
1048
+ };
1049
+ });
1050
+ // ─── 35. distance_to_resistance_R ────────────────────────────────────
1051
+ // Measures the distance from current price to the nearest swing high
1052
+ // (LONG) / swing low (SHORT) in units of ATR(14). Used to ensure a setup
1053
+ // has at least N R of headroom before hitting structural resistance.
1054
+ // params: minR, swingLookback, lookbackBars, tfHours
1055
+ register('distance_to_resistance_R', (ctx, params, direction) => {
1056
+ const minR = params.minR ?? 1.5;
1057
+ const swingLookback = Math.floor(params.swingLookback ?? 5);
1058
+ const lookbackBars = Math.floor(params.lookbackBars ?? 100);
1059
+ const tfHours = params.tfHours ?? 1;
1060
+ const bars = pickBars(ctx, tfHours);
1061
+ if (bars.length < lookbackBars) {
1062
+ return { met: false, value: 0, threshold: minR, description: `Insufficient ${tfHours}h bars for distance_to_resistance_R` };
1063
+ }
1064
+ if (ctx.atr14 <= 0) {
1065
+ return { met: false, value: 0, threshold: minR, description: 'ATR(14) is zero — cannot compute R distance' };
1066
+ }
1067
+ const window = bars.slice(-lookbackBars);
1068
+ const found = findPivotsIdx(window, swingLookback);
1069
+ const isLong = direction === 'LONG' || direction === null;
1070
+ const candidates = isLong
1071
+ ? found.highs.map((h) => h.price).filter((p) => p > ctx.currentPrice)
1072
+ : found.lows.map((l) => l.price).filter((p) => p < ctx.currentPrice);
1073
+ if (candidates.length === 0) {
1074
+ return {
1075
+ met: true,
1076
+ value: 999,
1077
+ threshold: minR,
1078
+ description: `No ${isLong ? 'overhead resistance' : 'support below'} in last ${lookbackBars} ${tfHours}h bars`,
1079
+ };
1080
+ }
1081
+ const nearest = isLong ? Math.min(...candidates) : Math.max(...candidates);
1082
+ const distance = Math.abs(nearest - ctx.currentPrice);
1083
+ const distanceR = distance / ctx.atr14;
1084
+ const met = distanceR >= minR;
1085
+ return {
1086
+ met,
1087
+ value: Math.round(distanceR * 100) / 100,
1088
+ threshold: minR,
1089
+ description: met
1090
+ ? `${distanceR.toFixed(2)}R clear to nearest ${isLong ? 'resistance' : 'support'} at ${nearest.toFixed(2)}`
1091
+ : `Only ${distanceR.toFixed(2)}R to nearest ${isLong ? 'resistance' : 'support'} at ${nearest.toFixed(2)} (need ≥${minR}R)`,
1092
+ };
1093
+ });
1094
+ // ─── 36. btc_rsi_overbought (skipIf helper) ──────────────────────────
1095
+ // Reads the global market context (BTC daily RSI) and returns met=true
1096
+ // when BTC RSI exceeds the threshold. Designed to be used in StrategyConfig.skipIf
1097
+ // to block longs when BTC daily RSI is overbought. Fail-open: if global
1098
+ // context is missing the condition returns met=false (do not skip).
1099
+ // params: maxRsi (default 70)
1100
+ register('btc_rsi_overbought', (ctx, params) => {
1101
+ const maxRsi = params.maxRsi ?? 70;
1102
+ const btcRsi = ctx.globalContext?.btcRsi14_1d;
1103
+ if (btcRsi === undefined) {
1104
+ return {
1105
+ met: false,
1106
+ value: 0,
1107
+ threshold: maxRsi,
1108
+ description: 'BTC 1d RSI unavailable in global context (fail-open)',
1109
+ };
1110
+ }
1111
+ const met = btcRsi > maxRsi;
1112
+ return {
1113
+ met,
1114
+ value: Math.round(btcRsi * 100) / 100,
1115
+ threshold: maxRsi,
1116
+ description: met
1117
+ ? `BTC 1d RSI ${btcRsi.toFixed(1)} > ${maxRsi} (overbought — skip longs)`
1118
+ : `BTC 1d RSI ${btcRsi.toFixed(1)} ≤ ${maxRsi}`,
1119
+ };
1120
+ });
1121
+ // ─── cascade_pulse_safety ──────────────────────────────────────────────
1122
+ // `met=true` when an `active_cascade` is in progress on the majors group
1123
+ // AND the dominant side opposes the trade direction — i.e. liquidations
1124
+ // are running INTO our entry. Use as skipIf to keep the agent out of
1125
+ // liquidation cascades that typically continue for 60-180 seconds.
1126
+ //
1127
+ // Params (ConditionConfig schema is `Record<string, number>` so flags are
1128
+ // numeric):
1129
+ // scope: 0 = majors (default), 1 = symbol
1130
+ // minSeverity: 1 = elevated, 2 = active_cascade (default)
1131
+ //
1132
+ // Fail-open semantics: returns met=false when the microstructure flag is
1133
+ // off (fields undefined) or events=0 in window (dominantSide=balanced).
1134
+ // This keeps the condition safe to add to a strategy before Phase 1 is on.
1135
+ register('cascade_pulse_safety', (ctx, params, direction) => {
1136
+ const useSymbolScope = params.scope === 1;
1137
+ const minSeverity = params.minSeverity === 1 ? 1 : 2; // default: active_cascade
1138
+ const classification = useSymbolScope ? ctx.cascadePulse60s : ctx.majorsCascadePulse60s;
1139
+ const dominant = ctx.cascadeDominantSide;
1140
+ const scopeLabel = useSymbolScope ? 'symbol' : 'majors';
1141
+ if (classification === undefined || dominant === undefined) {
1142
+ return {
1143
+ met: false,
1144
+ value: 0,
1145
+ threshold: 0,
1146
+ description: 'Cascade pulse unavailable (feature flag off — fail-open)',
1147
+ };
1148
+ }
1149
+ const severityRank = classification === 'active_cascade' ? 2 : classification === 'elevated' ? 1 : 0;
1150
+ const meetsSeverity = severityRank >= minSeverity;
1151
+ // Direction-aware: a long entry is endangered when LONGS are being
1152
+ // liquidated (cascading SELL pressure). A short entry is endangered when
1153
+ // SHORTS are being liquidated (cascading BUY pressure).
1154
+ const opposing = direction === 'LONG' ? dominant === 'long' :
1155
+ direction === 'SHORT' ? dominant === 'short' :
1156
+ false;
1157
+ const met = meetsSeverity && opposing;
1158
+ return {
1159
+ met,
1160
+ value: severityRank,
1161
+ threshold: minSeverity,
1162
+ description: met
1163
+ ? `${scopeLabel} cascade ${classification} dominant=${dominant} opposing ${direction} — skip`
1164
+ : `${scopeLabel} pulse=${classification}, dominant=${dominant}, dir=${direction} (no skip)`,
1165
+ };
1166
+ });
1167
+ // ─── resting_band_unfavourable ─────────────────────────────────────────
1168
+ // `met=true` when banded resting liquidity in the 0.5%/1% band shows the
1169
+ // opposite side has substantially more notional resting against the
1170
+ // trade direction — i.e. there's a wall of overhead supply against a
1171
+ // long, or a wall of underlying support against a short.
1172
+ //
1173
+ // Direction-aware:
1174
+ // LONG : skip if ask-side share of (mid, mid+0.5%] > minMargin (default 0.55)
1175
+ // SHORT : skip if bid-side share of (mid-0.5%, mid] > minMargin
1176
+ //
1177
+ // Fail-open: returns met=false (no skip) when bandedLiquidity flag is
1178
+ // off OR coverage_pct < minCoverage (data unreliable on tight books like
1179
+ // BTCUSDT where depth-20 reaches < 0.01%). Strategies on those symbols
1180
+ // effectively bypass this filter.
1181
+ //
1182
+ // Numeric params (ConditionConfig schema is Record<string, number>):
1183
+ // band: 5 = 0.5% (default), 10 = 1.0%
1184
+ // minMargin: 0.55 default — minimum share against direction to trigger
1185
+ // minCoverage: 0.5 default (% units) — coverage_pct floor for reliability
1186
+ register('resting_band_unfavourable', (ctx, params, direction) => {
1187
+ const useBand10 = params.band === 10;
1188
+ const minMargin = params.minMargin ?? 0.55;
1189
+ const minCoverage = params.minCoverage ?? 0.5;
1190
+ const imbalance = useBand10 ? ctx.restingBandImbalance10 : ctx.restingBandImbalance05;
1191
+ const coverage = ctx.restingCoveragePct;
1192
+ const bandLabel = useBand10 ? '1.0%' : '0.5%';
1193
+ if (imbalance === undefined || coverage === undefined) {
1194
+ return {
1195
+ met: false,
1196
+ value: 0,
1197
+ threshold: minMargin,
1198
+ description: 'Resting bands unavailable (feature flag off — fail-open)',
1199
+ };
1200
+ }
1201
+ if (coverage < minCoverage) {
1202
+ return {
1203
+ met: false,
1204
+ value: imbalance,
1205
+ threshold: minMargin,
1206
+ description: `Coverage ${coverage.toFixed(3)}% < ${minCoverage}% — depth-20 too tight, fail-open`,
1207
+ };
1208
+ }
1209
+ // imbalance is bid-share. ask-share = 1 - imbalance.
1210
+ // For LONG, "unfavourable" = ask-share dominant = imbalance < 1 - minMargin.
1211
+ // For SHORT, "unfavourable" = bid-share dominant = imbalance > minMargin.
1212
+ let unfavourable = false;
1213
+ let actualShare = 0;
1214
+ if (direction === 'LONG') {
1215
+ actualShare = 1 - imbalance; // ask side
1216
+ unfavourable = actualShare > minMargin;
1217
+ }
1218
+ else if (direction === 'SHORT') {
1219
+ actualShare = imbalance; // bid side
1220
+ unfavourable = actualShare > minMargin;
1221
+ }
1222
+ return {
1223
+ met: unfavourable,
1224
+ value: Math.round(actualShare * 1000) / 1000,
1225
+ threshold: minMargin,
1226
+ description: unfavourable
1227
+ ? `${bandLabel} band ${direction === 'LONG' ? 'ask' : 'bid'}-share ${(actualShare * 100).toFixed(1)}% > ${(minMargin * 100).toFixed(0)}% — wall against ${direction} entry, skip`
1228
+ : `${bandLabel} band ${direction === 'LONG' ? 'ask' : 'bid'}-share ${(actualShare * 100).toFixed(1)}%, coverage ${coverage.toFixed(2)}% (no skip)`,
1229
+ };
1230
+ });
1231
+ // ─── funding_extreme_skip ──────────────────────────────────────────────
1232
+ // `met=true` (skip) when current funding sits in the extreme tail of its
1233
+ // 30-day distribution AND the trade direction would be entering INTO that
1234
+ // crowded positioning:
1235
+ // - LONG skipped when percentileRank > upperRank (default 0.90)
1236
+ // — funding is in the top decile = crowded longs, top-tick risk
1237
+ // - SHORT skipped when percentileRank < lowerRank (default 0.10)
1238
+ // — funding is in the bottom decile = crowded shorts, squeeze risk
1239
+ //
1240
+ // Per-symbol adaptive by construction: the rank is computed against each
1241
+ // symbol's own 30d distribution server-side (see funding-percentile-api.ts),
1242
+ // so DOT/WIF heavy tails and BTC/BNB tight ranges both get sensible cutoffs.
1243
+ //
1244
+ // Numeric params:
1245
+ // upperRank: default 0.90 — skip LONG if rank > this
1246
+ // lowerRank: default 0.10 — skip SHORT if rank < this
1247
+ //
1248
+ // Fail-open: returns met=false when fundingPercentile is undefined
1249
+ // (FUNDING_OVERLAY flag off, insufficient sample, or symbol missing data).
1250
+ // Direction-aware via pass2 evaluation; in skipIf (direction=null) it
1251
+ // effectively no-ops since neither branch matches.
1252
+ register('funding_extreme_skip', (ctx, params, direction) => {
1253
+ const upperRank = params.upperRank ?? 0.90;
1254
+ const lowerRank = params.lowerRank ?? 0.10;
1255
+ const fp = ctx.fundingPercentile;
1256
+ if (!fp) {
1257
+ return {
1258
+ met: false,
1259
+ value: 0,
1260
+ threshold: 0,
1261
+ description: 'Funding percentile unavailable (FUNDING_OVERLAY off or no data — fail-open)',
1262
+ };
1263
+ }
1264
+ const rate_bps = fp.current * 10000;
1265
+ let met = false;
1266
+ let why = '';
1267
+ if (direction === 'LONG' && fp.percentileRank > upperRank) {
1268
+ met = true;
1269
+ why = `rank ${(fp.percentileRank * 100).toFixed(0)}th > ${(upperRank * 100).toFixed(0)}th — crowded longs (rate ${rate_bps.toFixed(2)} bps), skip LONG`;
1270
+ }
1271
+ else if (direction === 'SHORT' && fp.percentileRank < lowerRank) {
1272
+ met = true;
1273
+ why = `rank ${(fp.percentileRank * 100).toFixed(0)}th < ${(lowerRank * 100).toFixed(0)}th — crowded shorts (rate ${rate_bps.toFixed(2)} bps), skip SHORT`;
1274
+ }
1275
+ else {
1276
+ why = `rank ${(fp.percentileRank * 100).toFixed(0)}th, dir=${direction ?? 'null'} (no skip)`;
1277
+ }
1278
+ return {
1279
+ met,
1280
+ value: Math.round(fp.percentileRank * 100) / 100,
1281
+ threshold: direction === 'LONG' ? upperRank : direction === 'SHORT' ? lowerRank : 0,
1282
+ description: why,
1283
+ };
1284
+ });
1285
+ // ─── funding_position_ok ───────────────────────────────────────────────
1286
+ // Inverse of `funding_extreme_skip` for use in a strategy's `conditions`
1287
+ // list (gate semantics: met=true means "funding is OK to trade in this
1288
+ // direction"). Where `funding_extreme_skip` is intended for skipIf in
1289
+ // fixed-direction strategies, this variant is what regular conditions
1290
+ // pipelines need: pass-2 evaluation re-runs after directionRule resolves,
1291
+ // giving the agent direction-aware behaviour without changing the adapter.
1292
+ //
1293
+ // met=true when the trade is NOT entering INTO crowded positioning:
1294
+ // LONG acceptable if percentileRank ≤ upperRank (default 0.90)
1295
+ // SHORT acceptable if percentileRank ≥ lowerRank (default 0.10)
1296
+ //
1297
+ // Same numeric params as funding_extreme_skip. Same fail-open contract:
1298
+ // returns met=true when fundingPercentile is undefined (FUNDING_OVERLAY
1299
+ // off, no data, or insufficient sample) so the strategy is not blocked
1300
+ // by missing data.
1301
+ register('funding_position_ok', (ctx, params, direction) => {
1302
+ const upperRank = params.upperRank ?? 0.90;
1303
+ const lowerRank = params.lowerRank ?? 0.10;
1304
+ const fp = ctx.fundingPercentile;
1305
+ if (!fp) {
1306
+ return {
1307
+ met: true,
1308
+ value: 0,
1309
+ threshold: 0,
1310
+ description: 'Funding percentile unavailable (FUNDING_OVERLAY off or no data — fail-open)',
1311
+ };
1312
+ }
1313
+ const rate_bps = fp.current * 10000;
1314
+ let met = true;
1315
+ let why = '';
1316
+ if (direction === 'LONG' && fp.percentileRank > upperRank) {
1317
+ met = false;
1318
+ why = `rank ${(fp.percentileRank * 100).toFixed(0)}th > ${(upperRank * 100).toFixed(0)}th — crowded longs (rate ${rate_bps.toFixed(2)} bps), block LONG`;
1319
+ }
1320
+ else if (direction === 'SHORT' && fp.percentileRank < lowerRank) {
1321
+ met = false;
1322
+ why = `rank ${(fp.percentileRank * 100).toFixed(0)}th < ${(lowerRank * 100).toFixed(0)}th — crowded shorts (rate ${rate_bps.toFixed(2)} bps), block SHORT`;
1323
+ }
1324
+ else {
1325
+ why = `rank ${(fp.percentileRank * 100).toFixed(0)}th, dir=${direction ?? 'null'} (funding OK for this direction)`;
1326
+ }
1327
+ return {
1328
+ met,
1329
+ value: Math.round(fp.percentileRank * 100) / 100,
1330
+ threshold: direction === 'LONG' ? upperRank : direction === 'SHORT' ? lowerRank : 0,
1331
+ description: why,
1332
+ };
1333
+ });