@reefclaw/connect 0.1.0

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