@reefclaw/openclaw-plugin 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 (371) hide show
  1. package/.gitignore +8 -0
  2. package/audit/mode-transition-audit.d.ts +11 -0
  3. package/audit/mode-transition-audit.js +29 -0
  4. package/balance-utils.d.ts +36 -0
  5. package/balance-utils.js +98 -0
  6. package/bridge/bridge.d.ts +109 -0
  7. package/bridge/bridge.js +1036 -0
  8. package/bridge/config.d.ts +43 -0
  9. package/bridge/config.js +176 -0
  10. package/bridge/connector.d.ts +52 -0
  11. package/bridge/connector.js +293 -0
  12. package/bridge/event-replay-buffer.d.ts +43 -0
  13. package/bridge/event-replay-buffer.js +109 -0
  14. package/bridge/gateway/event-parser.d.ts +209 -0
  15. package/bridge/gateway/event-parser.js +793 -0
  16. package/bridge/gateway/gateway-config.d.ts +39 -0
  17. package/bridge/gateway/gateway-config.js +86 -0
  18. package/bridge/gateway/gateway-http-client.d.ts +50 -0
  19. package/bridge/gateway/gateway-http-client.js +165 -0
  20. package/bridge/gateway/gateway-ws-client.d.ts +116 -0
  21. package/bridge/gateway/gateway-ws-client.js +417 -0
  22. package/bridge/gateway/poller.d.ts +146 -0
  23. package/bridge/gateway/poller.js +505 -0
  24. package/bridge/gateway/tool-discovery.d.ts +25 -0
  25. package/bridge/gateway/tool-discovery.js +198 -0
  26. package/bridge/index.d.ts +2 -0
  27. package/bridge/index.js +253 -0
  28. package/bridge/logger.d.ts +2 -0
  29. package/bridge/logger.js +2 -0
  30. package/bridge/provider.d.ts +156 -0
  31. package/bridge/provider.js +2 -0
  32. package/bridge/providers/emergency-commands.d.ts +54 -0
  33. package/bridge/providers/emergency-commands.js +235 -0
  34. package/bridge/providers/gateway.d.ts +322 -0
  35. package/bridge/providers/gateway.js +2302 -0
  36. package/bridge/providers/mock.d.ts +37 -0
  37. package/bridge/providers/mock.js +385 -0
  38. package/bridge/providers/onboarding-commands.d.ts +83 -0
  39. package/bridge/providers/onboarding-commands.js +213 -0
  40. package/bridge/providers/risk-calculator.d.ts +96 -0
  41. package/bridge/providers/risk-calculator.js +369 -0
  42. package/bridge/setup.d.ts +32 -0
  43. package/bridge/setup.js +226 -0
  44. package/bridge/types.d.ts +584 -0
  45. package/bridge/types.js +50 -0
  46. package/bridge/utils/reconnect.d.ts +6 -0
  47. package/bridge/utils/reconnect.js +6 -0
  48. package/bridge/utils/skill-signing.d.ts +51 -0
  49. package/bridge/utils/skill-signing.js +138 -0
  50. package/bridge/utils/skill-version.d.ts +17 -0
  51. package/bridge/utils/skill-version.js +74 -0
  52. package/ccxt/binance-ban-gate.d.ts +47 -0
  53. package/ccxt/binance-ban-gate.js +409 -0
  54. package/ccxt/binance-private.d.ts +325 -0
  55. package/ccxt/binance-private.js +1415 -0
  56. package/ccxt/binance-public.d.ts +18 -0
  57. package/ccxt/binance-public.js +147 -0
  58. package/config/agent-config-client.d.ts +55 -0
  59. package/config/agent-config-client.js +145 -0
  60. package/config/agent-config-poller.d.ts +25 -0
  61. package/config/agent-config-poller.js +100 -0
  62. package/config/brackets-config.d.ts +22 -0
  63. package/config/brackets-config.js +58 -0
  64. package/config/gate-store.d.ts +18 -0
  65. package/config/gate-store.js +61 -0
  66. package/config/plugin-config-io.d.ts +181 -0
  67. package/config/plugin-config-io.js +84 -0
  68. package/config/position-review-config.d.ts +35 -0
  69. package/config/position-review-config.js +105 -0
  70. package/config/tool-gate.d.ts +53 -0
  71. package/config/tool-gate.js +125 -0
  72. package/config/user-data-stream-config.d.ts +85 -0
  73. package/config/user-data-stream-config.js +224 -0
  74. package/connector-supervisor.d.ts +36 -0
  75. package/connector-supervisor.js +149 -0
  76. package/exchange-adapter.d.ts +49 -0
  77. package/exchange-adapter.js +4 -0
  78. package/index.d.ts +30 -0
  79. package/index.js +2030 -0
  80. package/ingest/pending-entry-metadata.d.ts +52 -0
  81. package/ingest/pending-entry-metadata.js +182 -0
  82. package/ingest/position-auto-capture.d.ts +98 -0
  83. package/ingest/position-auto-capture.js +394 -0
  84. package/ingest/position-decisions-client.d.ts +318 -0
  85. package/ingest/position-decisions-client.js +296 -0
  86. package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
  87. package/ingest/reconcile-db-vs-exchange.js +114 -0
  88. package/ingest/reconciler-cleanup.d.ts +37 -0
  89. package/ingest/reconciler-cleanup.js +147 -0
  90. package/ingest/rest-gap-filler.d.ts +191 -0
  91. package/ingest/rest-gap-filler.js +565 -0
  92. package/ingest/touched-symbols-store.d.ts +25 -0
  93. package/ingest/touched-symbols-store.js +96 -0
  94. package/ingest/trade-store-client.d.ts +40 -0
  95. package/ingest/trade-store-client.js +125 -0
  96. package/ingest/ws-ingest.d.ts +43 -0
  97. package/ingest/ws-ingest.js +126 -0
  98. package/learning/setup-family.d.ts +21 -0
  99. package/learning/setup-family.js +103 -0
  100. package/lifecycle/install-signal-handlers.d.ts +33 -0
  101. package/lifecycle/install-signal-handlers.js +112 -0
  102. package/lifecycle/shutdown-coordinator.d.ts +43 -0
  103. package/lifecycle/shutdown-coordinator.js +131 -0
  104. package/live/bracket-id.d.ts +18 -0
  105. package/live/bracket-id.js +81 -0
  106. package/live/bracket-ledger.d.ts +54 -0
  107. package/live/bracket-ledger.js +267 -0
  108. package/live/bracket-manager.d.ts +82 -0
  109. package/live/bracket-manager.js +478 -0
  110. package/live/bracket-params.d.ts +22 -0
  111. package/live/bracket-params.js +124 -0
  112. package/live/bracket-reconciler.d.ts +95 -0
  113. package/live/bracket-reconciler.js +573 -0
  114. package/live/bracket-types.d.ts +102 -0
  115. package/live/bracket-types.js +8 -0
  116. package/live/deposit-tracker.d.ts +62 -0
  117. package/live/deposit-tracker.js +97 -0
  118. package/live/emergency-controls.d.ts +32 -0
  119. package/live/emergency-controls.js +226 -0
  120. package/live/exchange-errors.d.ts +12 -0
  121. package/live/exchange-errors.js +130 -0
  122. package/live/exchange-info-cache.d.ts +35 -0
  123. package/live/exchange-info-cache.js +119 -0
  124. package/live/fact-subscriber.d.ts +78 -0
  125. package/live/fact-subscriber.js +182 -0
  126. package/live/intent-journal.d.ts +42 -0
  127. package/live/intent-journal.js +122 -0
  128. package/live/listen-key-manager.d.ts +70 -0
  129. package/live/listen-key-manager.js +169 -0
  130. package/live/live-adapter.d.ts +264 -0
  131. package/live/live-adapter.js +1665 -0
  132. package/live/live-balance-enricher.d.ts +32 -0
  133. package/live/live-balance-enricher.js +104 -0
  134. package/live/live-bracket-api.d.ts +13 -0
  135. package/live/live-bracket-api.js +20 -0
  136. package/live/live-state-store.d.ts +194 -0
  137. package/live/live-state-store.js +450 -0
  138. package/live/local-signal-service.d.ts +57 -0
  139. package/live/local-signal-service.js +146 -0
  140. package/live/local-strategy-evaluator.d.ts +62 -0
  141. package/live/local-strategy-evaluator.js +127 -0
  142. package/live/microstructure-assembler.d.ts +54 -0
  143. package/live/microstructure-assembler.js +148 -0
  144. package/live/order-poller.d.ts +29 -0
  145. package/live/order-poller.js +125 -0
  146. package/live/position-state-store.d.ts +83 -0
  147. package/live/position-state-store.js +237 -0
  148. package/live/proposal-decision-listener.d.ts +64 -0
  149. package/live/proposal-decision-listener.js +288 -0
  150. package/live/proposal-manager.d.ts +76 -0
  151. package/live/proposal-manager.js +140 -0
  152. package/live/rate-limiter.d.ts +47 -0
  153. package/live/rate-limiter.js +159 -0
  154. package/live/reconciler.d.ts +39 -0
  155. package/live/reconciler.js +175 -0
  156. package/live/setup-buckets.d.ts +7 -0
  157. package/live/setup-buckets.js +33 -0
  158. package/live/slippage-tracker.d.ts +45 -0
  159. package/live/slippage-tracker.js +78 -0
  160. package/live/stop-watcher.d.ts +34 -0
  161. package/live/stop-watcher.js +158 -0
  162. package/live/user-data-active-probe.d.ts +54 -0
  163. package/live/user-data-active-probe.js +180 -0
  164. package/live/user-data-stream-controller.d.ts +200 -0
  165. package/live/user-data-stream-controller.js +579 -0
  166. package/live/user-data-stream-ws.d.ts +22 -0
  167. package/live/user-data-stream-ws.js +63 -0
  168. package/live/user-data-stream.d.ts +243 -0
  169. package/live/user-data-stream.js +704 -0
  170. package/logger.d.ts +2 -0
  171. package/logger.js +2 -0
  172. package/mfe.d.ts +21 -0
  173. package/mfe.js +68 -0
  174. package/onboarding/mode-ladder.d.ts +1 -0
  175. package/onboarding/mode-ladder.js +3 -0
  176. package/onboarding/runtime.d.ts +71 -0
  177. package/onboarding/runtime.js +153 -0
  178. package/openclaw.plugin.json +94 -0
  179. package/package.json +27 -0
  180. package/paper-adapter.d.ts +24 -0
  181. package/paper-adapter.js +91 -0
  182. package/persistence/state-manager.d.ts +42 -0
  183. package/persistence/state-manager.js +164 -0
  184. package/pinned-plan.d.ts +9 -0
  185. package/pinned-plan.js +23 -0
  186. package/risk/pre-trade-check.d.ts +38 -0
  187. package/risk/pre-trade-check.js +345 -0
  188. package/risk/pre-trade-types.d.ts +60 -0
  189. package/risk/pre-trade-types.js +3 -0
  190. package/scripts/assemble.mjs +114 -0
  191. package/shadow/shadow-tracker.d.ts +36 -0
  192. package/shadow/shadow-tracker.js +151 -0
  193. package/shadow/types.d.ts +42 -0
  194. package/shadow/types.js +20 -0
  195. package/shared/indicators-extended.d.ts +52 -0
  196. package/shared/indicators-extended.js +291 -0
  197. package/shared/indicators.d.ts +15 -0
  198. package/shared/indicators.js +114 -0
  199. package/signals/conditions/registry.d.ts +16 -0
  200. package/signals/conditions/registry.js +1274 -0
  201. package/signals/conditions/types.d.ts +1 -0
  202. package/signals/conditions/types.js +4 -0
  203. package/signals/direction-rules.d.ts +3 -0
  204. package/signals/direction-rules.js +24 -0
  205. package/signals/entry-rules.d.ts +6 -0
  206. package/signals/entry-rules.js +33 -0
  207. package/signals/serialize-context.d.ts +4 -0
  208. package/signals/serialize-context.js +39 -0
  209. package/signals/stop-rules.d.ts +3 -0
  210. package/signals/stop-rules.js +48 -0
  211. package/signals/strategy-adapter.d.ts +14 -0
  212. package/signals/strategy-adapter.js +122 -0
  213. package/signals/types.d.ts +1 -0
  214. package/signals/types.js +8 -0
  215. package/simulator/exchange-simulator.d.ts +93 -0
  216. package/simulator/exchange-simulator.js +684 -0
  217. package/simulator/fill-engine.d.ts +53 -0
  218. package/simulator/fill-engine.js +276 -0
  219. package/simulator/paper-market-feed.d.ts +26 -0
  220. package/simulator/paper-market-feed.js +104 -0
  221. package/simulator/realistic-fills.d.ts +59 -0
  222. package/simulator/realistic-fills.js +175 -0
  223. package/simulator/types.d.ts +219 -0
  224. package/simulator/types.js +43 -0
  225. package/skills/reefclaw/SKILL.md +97 -0
  226. package/strategy/builtin-strategies.d.ts +2 -0
  227. package/strategy/builtin-strategies.js +109 -0
  228. package/strategy/condition-registry.d.ts +3 -0
  229. package/strategy/condition-registry.js +153 -0
  230. package/strategy/evaluator.d.ts +67 -0
  231. package/strategy/evaluator.js +93 -0
  232. package/tools/assessment-validation.d.ts +118 -0
  233. package/tools/assessment-validation.js +415 -0
  234. package/tools/attach-brackets.d.ts +34 -0
  235. package/tools/attach-brackets.js +363 -0
  236. package/tools/audit-bracket-protection.d.ts +49 -0
  237. package/tools/audit-bracket-protection.js +527 -0
  238. package/tools/cancel-all-orders.d.ts +7 -0
  239. package/tools/cancel-all-orders.js +5 -0
  240. package/tools/cancel-order.d.ts +10 -0
  241. package/tools/cancel-order.js +14 -0
  242. package/tools/check-position-health.d.ts +46 -0
  243. package/tools/check-position-health.js +194 -0
  244. package/tools/clear-exchange-credentials.d.ts +24 -0
  245. package/tools/clear-exchange-credentials.js +70 -0
  246. package/tools/close-position.d.ts +22 -0
  247. package/tools/close-position.js +449 -0
  248. package/tools/create-order.d.ts +54 -0
  249. package/tools/create-order.js +338 -0
  250. package/tools/exit-gate.d.ts +58 -0
  251. package/tools/exit-gate.js +162 -0
  252. package/tools/fetch-balance.d.ts +5 -0
  253. package/tools/fetch-balance.js +4 -0
  254. package/tools/fetch-ohlcv.d.ts +11 -0
  255. package/tools/fetch-ohlcv.js +8 -0
  256. package/tools/fetch-open-orders.d.ts +7 -0
  257. package/tools/fetch-open-orders.js +4 -0
  258. package/tools/fetch-positions.d.ts +7 -0
  259. package/tools/fetch-positions.js +4 -0
  260. package/tools/fetch-ticker.d.ts +11 -0
  261. package/tools/fetch-ticker.js +5 -0
  262. package/tools/get-agent-profile.d.ts +4 -0
  263. package/tools/get-agent-profile.js +6 -0
  264. package/tools/get-analytics.d.ts +6 -0
  265. package/tools/get-analytics.js +7 -0
  266. package/tools/get-backtest.d.ts +12 -0
  267. package/tools/get-backtest.js +91 -0
  268. package/tools/get-basis.d.ts +7 -0
  269. package/tools/get-basis.js +7 -0
  270. package/tools/get-bracket-config.d.ts +11 -0
  271. package/tools/get-bracket-config.js +24 -0
  272. package/tools/get-cascade-risk.d.ts +7 -0
  273. package/tools/get-cascade-risk.js +8 -0
  274. package/tools/get-crypto-metrics.d.ts +18 -0
  275. package/tools/get-crypto-metrics.js +45 -0
  276. package/tools/get-cvd.d.ts +6 -0
  277. package/tools/get-cvd.js +6 -0
  278. package/tools/get-divergences.d.ts +6 -0
  279. package/tools/get-divergences.js +6 -0
  280. package/tools/get-funding-context.d.ts +6 -0
  281. package/tools/get-funding-context.js +16 -0
  282. package/tools/get-liquidation-levels.d.ts +7 -0
  283. package/tools/get-liquidation-levels.js +7 -0
  284. package/tools/get-liquidation-pulse.d.ts +9 -0
  285. package/tools/get-liquidation-pulse.js +22 -0
  286. package/tools/get-market-breadth.d.ts +6 -0
  287. package/tools/get-market-breadth.js +8 -0
  288. package/tools/get-market-intel.d.ts +19 -0
  289. package/tools/get-market-intel.js +116 -0
  290. package/tools/get-market-structure.d.ts +47 -0
  291. package/tools/get-market-structure.js +198 -0
  292. package/tools/get-my-mined-patterns.d.ts +20 -0
  293. package/tools/get-my-mined-patterns.js +61 -0
  294. package/tools/get-my-proposed-learnings.d.ts +20 -0
  295. package/tools/get-my-proposed-learnings.js +55 -0
  296. package/tools/get-my-recent-reviews.d.ts +22 -0
  297. package/tools/get-my-recent-reviews.js +66 -0
  298. package/tools/get-orderbook.d.ts +21 -0
  299. package/tools/get-orderbook.js +32 -0
  300. package/tools/get-pattern-scan.d.ts +7 -0
  301. package/tools/get-pattern-scan.js +8 -0
  302. package/tools/get-regime.d.ts +6 -0
  303. package/tools/get-regime.js +7 -0
  304. package/tools/get-relevant-learnings.d.ts +21 -0
  305. package/tools/get-relevant-learnings.js +65 -0
  306. package/tools/get-resting-liquidity.d.ts +6 -0
  307. package/tools/get-resting-liquidity.js +11 -0
  308. package/tools/get-risk-scenario.d.ts +29 -0
  309. package/tools/get-risk-scenario.js +47 -0
  310. package/tools/get-risk-summary.d.ts +51 -0
  311. package/tools/get-risk-summary.js +118 -0
  312. package/tools/get-sentiment.d.ts +4 -0
  313. package/tools/get-sentiment.js +6 -0
  314. package/tools/get-session-review.d.ts +7 -0
  315. package/tools/get-session-review.js +8 -0
  316. package/tools/get-setup-detail.d.ts +7 -0
  317. package/tools/get-setup-detail.js +303 -0
  318. package/tools/get-signals.d.ts +15 -0
  319. package/tools/get-signals.js +54 -0
  320. package/tools/get-sizing.d.ts +6 -0
  321. package/tools/get-sizing.js +6 -0
  322. package/tools/get-trade-feedback.d.ts +7 -0
  323. package/tools/get-trade-feedback.js +8 -0
  324. package/tools/get-trade-flow.d.ts +7 -0
  325. package/tools/get-trade-flow.js +7 -0
  326. package/tools/get-volume-analysis.d.ts +21 -0
  327. package/tools/get-volume-analysis.js +74 -0
  328. package/tools/get-volume-profile.d.ts +7 -0
  329. package/tools/get-volume-profile.js +7 -0
  330. package/tools/helpers.d.ts +25 -0
  331. package/tools/helpers.js +38 -0
  332. package/tools/intel-api.d.ts +14 -0
  333. package/tools/intel-api.js +52 -0
  334. package/tools/intel-cache.d.ts +25 -0
  335. package/tools/intel-cache.js +133 -0
  336. package/tools/list-strategies.d.ts +7 -0
  337. package/tools/list-strategies.js +6 -0
  338. package/tools/modify-stop.d.ts +17 -0
  339. package/tools/modify-stop.js +81 -0
  340. package/tools/modify-target.d.ts +17 -0
  341. package/tools/modify-target.js +71 -0
  342. package/tools/propose-learning.d.ts +22 -0
  343. package/tools/propose-learning.js +65 -0
  344. package/tools/query-review-outcomes.d.ts +30 -0
  345. package/tools/query-review-outcomes.js +64 -0
  346. package/tools/query-trades.d.ts +21 -0
  347. package/tools/query-trades.js +37 -0
  348. package/tools/record-position-reviews.d.ts +38 -0
  349. package/tools/record-position-reviews.js +147 -0
  350. package/tools/save-strategy.d.ts +16 -0
  351. package/tools/save-strategy.js +46 -0
  352. package/tools/scan-pairs.d.ts +18 -0
  353. package/tools/scan-pairs.js +220 -0
  354. package/tools/score-setup.d.ts +31 -0
  355. package/tools/score-setup.js +268 -0
  356. package/tools/set-bracket-requirement.d.ts +18 -0
  357. package/tools/set-bracket-requirement.js +81 -0
  358. package/tools/set-exchange-credentials.d.ts +25 -0
  359. package/tools/set-exchange-credentials.js +80 -0
  360. package/tools/set-trading-mode.d.ts +26 -0
  361. package/tools/set-trading-mode.js +135 -0
  362. package/tools/test-exchange-credentials.d.ts +16 -0
  363. package/tools/test-exchange-credentials.js +100 -0
  364. package/tools/toggle-strategy.d.ts +8 -0
  365. package/tools/toggle-strategy.js +8 -0
  366. package/trading-params-cache.d.ts +26 -0
  367. package/trading-params-cache.js +52 -0
  368. package/types.d.ts +110 -0
  369. package/types.js +7 -0
  370. package/util/plugin-paths.d.ts +3 -0
  371. package/util/plugin-paths.js +15 -0
@@ -0,0 +1,164 @@
1
+ // State persistence manager for the paper trading simulator.
2
+ // Saves/loads state to ~/.openclaw/plugins/reefclaw-paper-trading/state.json
3
+ // Uses atomic writes (temp file + rename) to prevent corruption.
4
+ import { readFile, writeFile, rename, mkdir, unlink } from 'node:fs/promises';
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { join, dirname } from 'node:path';
8
+ import { randomUUID } from 'node:crypto';
9
+ import { logger } from '../logger.js';
10
+ import { createDefaultState } from '../simulator/types.js';
11
+ const TAG = 'state-manager';
12
+ export class StateManager {
13
+ statePath;
14
+ saveTimer = null;
15
+ pendingState = null;
16
+ constructor(pluginId, baseDir) {
17
+ const base = baseDir ?? join(homedir(), '.openclaw', 'plugins', pluginId);
18
+ this.statePath = join(base, 'state.json');
19
+ }
20
+ getStatePath() {
21
+ return this.statePath;
22
+ }
23
+ /** Quarantine a state file that exists but won't parse, instead of letting
24
+ * the caller silently overwrite it with a fresh default (which would wipe
25
+ * the positions/orders/realized-PnL ledger). The corrupt bytes are renamed
26
+ * to `<state>.corrupt-<ts>` so an operator can recover them. Returns true if
27
+ * a corrupt file was quarantined. */
28
+ quarantineCorruptSync() {
29
+ if (!existsSync(this.statePath))
30
+ return false;
31
+ const dest = `${this.statePath}.corrupt-${Date.now()}`;
32
+ try {
33
+ renameSync(this.statePath, dest);
34
+ logger.error(TAG, `State file at ${this.statePath} is unreadable/corrupt — quarantined to ${dest}. NOT resetting silently; a fresh default will be used. Investigate before trading.`);
35
+ }
36
+ catch (err) {
37
+ logger.error(TAG, `State file at ${this.statePath} is corrupt AND could not be quarantined: ${err instanceof Error ? err.message : String(err)}`);
38
+ }
39
+ return true;
40
+ }
41
+ /** Load state from disk. Returns null when the file is MISSING (fresh start)
42
+ * OR when it exists-but-is-unreadable (in which case the corrupt bytes are
43
+ * quarantined first — never silently discarded). The two cases are
44
+ * distinguished in the log; both yield null so the caller seeds a default,
45
+ * but a corrupt file is preserved as `<state>.corrupt-<ts>`. */
46
+ async load() {
47
+ if (!existsSync(this.statePath)) {
48
+ logger.info(TAG, `No state file found at ${this.statePath}`);
49
+ return null;
50
+ }
51
+ try {
52
+ const raw = await readFile(this.statePath, 'utf-8');
53
+ const state = JSON.parse(raw);
54
+ logger.info(TAG, `Loaded state: ${state.positions.length} positions, ${state.openOrders.length} orders, ${state.tradeHistory.length} trades`);
55
+ return state;
56
+ }
57
+ catch {
58
+ // Exists but unreadable/corrupt — preserve, don't silently reset.
59
+ this.quarantineCorruptSync();
60
+ return null;
61
+ }
62
+ }
63
+ /** Save state atomically (write to a UNIQUE temp, rename to final). A unique
64
+ * temp name (pid + uuid) is essential under the two-process model: both the
65
+ * agent and gateway processes wire stateChanged→save, and a shared
66
+ * `<state>.tmp` let their concurrent writes interleave into a torn file that
67
+ * then parses as garbage on next boot. With a per-write temp, the worst case
68
+ * degrades to a harmless last-writer-wins rename of two COMPLETE files. */
69
+ async save(state) {
70
+ const tmpPath = `${this.statePath}.${process.pid}.${randomUUID()}.tmp`;
71
+ try {
72
+ const dir = dirname(this.statePath);
73
+ if (!existsSync(dir)) {
74
+ await mkdir(dir, { recursive: true });
75
+ }
76
+ await writeFile(tmpPath, JSON.stringify(state, null, 2), 'utf-8');
77
+ await rename(tmpPath, this.statePath);
78
+ logger.debug(TAG, 'State saved');
79
+ }
80
+ catch (err) {
81
+ logger.error(TAG, `Failed to save state: ${err instanceof Error ? err.message : String(err)}`);
82
+ // Best-effort cleanup of the orphaned temp so they don't accumulate.
83
+ await unlink(tmpPath).catch(() => { });
84
+ }
85
+ }
86
+ /**
87
+ * Debounced save — coalesces rapid mutations into one write per second.
88
+ * Call this on every state change; it will save at most once per second.
89
+ */
90
+ debouncedSave(state) {
91
+ this.pendingState = state;
92
+ if (this.saveTimer)
93
+ return; // Already scheduled
94
+ this.saveTimer = setTimeout(() => {
95
+ this.saveTimer = null;
96
+ if (this.pendingState) {
97
+ const s = this.pendingState;
98
+ this.pendingState = null;
99
+ this.save(s).catch(() => { });
100
+ }
101
+ }, 1000);
102
+ }
103
+ /** Flush any pending debounced save immediately. */
104
+ async flush() {
105
+ if (this.saveTimer) {
106
+ clearTimeout(this.saveTimer);
107
+ this.saveTimer = null;
108
+ }
109
+ if (this.pendingState) {
110
+ const s = this.pendingState;
111
+ this.pendingState = null;
112
+ await this.save(s);
113
+ }
114
+ }
115
+ /** Load state or create default if not found. */
116
+ async loadOrDefault(startingBalance, quoteCurrency) {
117
+ const loaded = await this.load();
118
+ if (loaded)
119
+ return loaded;
120
+ const state = createDefaultState(startingBalance, quoteCurrency);
121
+ logger.info(TAG, `Initialized default state: ${startingBalance} ${quoteCurrency}`);
122
+ await this.save(state);
123
+ return state;
124
+ }
125
+ /** Synchronous load — for use in synchronous plugin register(). Same
126
+ * missing-vs-corrupt distinction as load(): a corrupt file is quarantined
127
+ * (preserved), never silently overwritten by the default the caller seeds. */
128
+ loadSync() {
129
+ if (!existsSync(this.statePath)) {
130
+ logger.info(TAG, `No state file found at ${this.statePath}`);
131
+ return null;
132
+ }
133
+ try {
134
+ const raw = readFileSync(this.statePath, 'utf-8');
135
+ const state = JSON.parse(raw);
136
+ logger.info(TAG, `Loaded state: ${state.positions.length} positions, ${state.openOrders.length} orders, ${state.tradeHistory.length} trades`);
137
+ return state;
138
+ }
139
+ catch {
140
+ this.quarantineCorruptSync();
141
+ return null;
142
+ }
143
+ }
144
+ /** Synchronous load-or-create-default — for use in synchronous plugin register(). */
145
+ loadOrDefaultSync(startingBalance, quoteCurrency) {
146
+ const loaded = this.loadSync();
147
+ if (loaded)
148
+ return loaded;
149
+ const state = createDefaultState(startingBalance, quoteCurrency);
150
+ logger.info(TAG, `Initialized default state: ${startingBalance} ${quoteCurrency}`);
151
+ // Save synchronously so state persists immediately
152
+ try {
153
+ const dir = dirname(this.statePath);
154
+ if (!existsSync(dir)) {
155
+ mkdirSync(dir, { recursive: true });
156
+ }
157
+ writeFileSync(this.statePath, JSON.stringify(state, null, 2), 'utf-8');
158
+ }
159
+ catch (err) {
160
+ logger.error(TAG, `Failed to save initial state: ${err instanceof Error ? err.message : String(err)}`);
161
+ }
162
+ return state;
163
+ }
164
+ }
@@ -0,0 +1,9 @@
1
+ /** Has the agent's pinned invalidation price been reached at the current mark?
2
+ *
3
+ * Long: invalidated when mark <= invalidationPrice.
4
+ * Short: invalidated when mark >= invalidationPrice.
5
+ *
6
+ * Returns undefined when there is nothing to check — no pinned invalidation,
7
+ * or no usable mark — so a missing plan reads as "unknown", never a false
8
+ * "not hit". Pure. */
9
+ export declare function computeInvalidationHit(side: 'long' | 'short', markPrice: number | undefined, invalidationPrice: number | undefined): boolean | undefined;
package/pinned-plan.js ADDED
@@ -0,0 +1,23 @@
1
+ // Pinned trade plan — derived indication helpers (Slice 1, Task 5).
2
+ //
3
+ // Pure functions surfaced to the agent at heartbeat (via fetch_positions →
4
+ // CcxtPosition) so it can hold to its OWN plan instead of re-litigating the
5
+ // entry every cycle. Indication only — nothing here enforces anything; the
6
+ // only forced thing on the position is that a protective stop exists.
7
+ /** Has the agent's pinned invalidation price been reached at the current mark?
8
+ *
9
+ * Long: invalidated when mark <= invalidationPrice.
10
+ * Short: invalidated when mark >= invalidationPrice.
11
+ *
12
+ * Returns undefined when there is nothing to check — no pinned invalidation,
13
+ * or no usable mark — so a missing plan reads as "unknown", never a false
14
+ * "not hit". Pure. */
15
+ export function computeInvalidationHit(side, markPrice, invalidationPrice) {
16
+ if (invalidationPrice == null ||
17
+ !Number.isFinite(invalidationPrice) ||
18
+ markPrice == null ||
19
+ !Number.isFinite(markPrice)) {
20
+ return undefined;
21
+ }
22
+ return side === 'long' ? markPrice <= invalidationPrice : markPrice >= invalidationPrice;
23
+ }
@@ -0,0 +1,38 @@
1
+ import type { DrawdownZone, PreTradeRiskLimits, PreTradeResult, ProposedOrder, PortfolioSnapshot, PreTradeOptions } from './pre-trade-types.js';
2
+ export declare function getDefaultPreTradeLimits(): PreTradeRiskLimits;
3
+ /** @deprecated Use getDefaultPreTradeLimits() for dynamic values */
4
+ export declare const DEFAULT_PRE_TRADE_LIMITS: PreTradeRiskLimits;
5
+ export declare function getDrawdownThresholds(): {
6
+ YELLOW: number;
7
+ ORANGE: number;
8
+ RED: number;
9
+ };
10
+ export declare const DRAWDOWN_ZONE_THRESHOLDS: {
11
+ readonly YELLOW: -0.01;
12
+ readonly ORANGE: -0.02;
13
+ readonly RED: -0.025;
14
+ };
15
+ /** Determine drawdown zone from drawdown ratio (e.g. -0.015 = -1.5%). */
16
+ export declare function getDrawdownZone(drawdownRatio: number): DrawdownZone;
17
+ /** Compute volatility factor from current ATR vs baseline. Clamped [0.5, 3.0]. */
18
+ export declare function computeVolFactor(currentAtr: number, baselineAtr: number): number;
19
+ /** Adjust a limit value by volatility factor. Higher vol = tighter limit. */
20
+ export declare function adjustLimitByVol(defaultLimit: number, volFactor: number): number;
21
+ /** Check if an order is closing an existing position (partial or full). */
22
+ export declare function isClosingOrder(order: ProposedOrder, positions: PortfolioSnapshot['positions']): boolean;
23
+ export declare const EQUITY_FLOOR: {
24
+ /** Max single-position notional as a fraction of equity.
25
+ * Raised 0.25 → 0.40 on 2026-04-20 so small live accounts ($186) can fit
26
+ * BTC's ~$75 min-lot. Still protects against all-in entries. Tracked for
27
+ * promotion to operator-configurable trading param in the next session. */
28
+ readonly MAX_POSITION_PCT: 0.4;
29
+ /** Max per-trade risk (expected loss) as a fraction of equity. */
30
+ readonly MAX_PER_TRADE_LOSS_PCT: 0.02;
31
+ /** Presumed adverse move when an order has no explicit stopPrice. */
32
+ readonly NO_STOP_ASSUMED_ADVERSE: 0.05;
33
+ };
34
+ export declare function preTradeRiskCheck(order: ProposedOrder, portfolio: PortfolioSnapshot, limits?: PreTradeRiskLimits, options?: PreTradeOptions): PreTradeResult;
35
+ /** Count consecutive losses from the end of a trade history array. */
36
+ export declare function computeConsecutiveLosses(trades: Array<{
37
+ realizedPnl: number;
38
+ }>): number;
@@ -0,0 +1,345 @@
1
+ // Pre-trade risk gate — pure function.
2
+ // Validates a proposed order against portfolio limits, drawdown zones, and volatility.
3
+ // Lives in the plugin (synchronous access to simulator state).
4
+ import { getCachedTradingParams } from '../trading-params-cache.js';
5
+ // ---- Default pre-trade limits (now read from trading params cache) ----
6
+ export function getDefaultPreTradeLimits() {
7
+ const tp = getCachedTradingParams();
8
+ return {
9
+ maxPositionSize: tp.maxPositionSize,
10
+ maxOpenPositions: tp.maxOpenPositions,
11
+ maxGrossExposure: tp.maxGrossExposure,
12
+ maxPerTradeLoss: tp.maxPerTradeLoss,
13
+ };
14
+ }
15
+ /** @deprecated Use getDefaultPreTradeLimits() for dynamic values */
16
+ export const DEFAULT_PRE_TRADE_LIMITS = {
17
+ maxPositionSize: 10_000,
18
+ maxOpenPositions: 5,
19
+ maxGrossExposure: 1.5,
20
+ maxPerTradeLoss: 500,
21
+ };
22
+ // ---- Drawdown zone thresholds (now read from trading params cache) ----
23
+ export function getDrawdownThresholds() {
24
+ const tp = getCachedTradingParams();
25
+ return {
26
+ YELLOW: tp.drawdownYellow,
27
+ ORANGE: tp.drawdownOrange,
28
+ RED: tp.drawdownRed,
29
+ };
30
+ }
31
+ export const DRAWDOWN_ZONE_THRESHOLDS = {
32
+ YELLOW: -0.01, // -1%
33
+ ORANGE: -0.02, // -2%
34
+ RED: -0.025, // -2.5%
35
+ };
36
+ // ---- Pure helpers ----
37
+ /** Determine drawdown zone from drawdown ratio (e.g. -0.015 = -1.5%). */
38
+ export function getDrawdownZone(drawdownRatio) {
39
+ const thresholds = getDrawdownThresholds();
40
+ if (drawdownRatio <= thresholds.RED)
41
+ return 'RED';
42
+ if (drawdownRatio <= thresholds.ORANGE)
43
+ return 'ORANGE';
44
+ if (drawdownRatio <= thresholds.YELLOW)
45
+ return 'YELLOW';
46
+ return 'GREEN';
47
+ }
48
+ /** Compute volatility factor from current ATR vs baseline. Clamped [0.5, 3.0]. */
49
+ export function computeVolFactor(currentAtr, baselineAtr) {
50
+ if (baselineAtr <= 0 || currentAtr <= 0)
51
+ return 1.0;
52
+ return Math.max(0.5, Math.min(3.0, currentAtr / baselineAtr));
53
+ }
54
+ /** Adjust a limit value by volatility factor. Higher vol = tighter limit. */
55
+ export function adjustLimitByVol(defaultLimit, volFactor) {
56
+ if (volFactor <= 1.0)
57
+ return defaultLimit;
58
+ return defaultLimit / volFactor;
59
+ }
60
+ /** Check if an order is closing an existing position (partial or full). */
61
+ export function isClosingOrder(order, positions) {
62
+ const pos = positions.find(p => p.symbol === order.symbol);
63
+ if (!pos)
64
+ return false;
65
+ // Sell against a long = closing; buy against a short = closing
66
+ return (pos.side === 'long' && order.side === 'sell') ||
67
+ (pos.side === 'short' && order.side === 'buy');
68
+ }
69
+ // ---- Main pre-trade gate ----
70
+ // ---- Equity-proportional floors (safety net for tiny accounts) ----
71
+ // These are independent of the configurable trading params and exist so that
72
+ // a $10,000 static `maxPositionSize` default can't allow a $289 account to
73
+ // open an $8,000 position. They shrink the effective caps relative to current
74
+ // equity; the tighter of (absolute limit, equity-proportional floor) wins.
75
+ export const EQUITY_FLOOR = {
76
+ /** Max single-position notional as a fraction of equity.
77
+ * Raised 0.25 → 0.40 on 2026-04-20 so small live accounts ($186) can fit
78
+ * BTC's ~$75 min-lot. Still protects against all-in entries. Tracked for
79
+ * promotion to operator-configurable trading param in the next session. */
80
+ MAX_POSITION_PCT: 0.40,
81
+ /** Max per-trade risk (expected loss) as a fraction of equity. */
82
+ MAX_PER_TRADE_LOSS_PCT: 0.02,
83
+ /** Presumed adverse move when an order has no explicit stopPrice. */
84
+ NO_STOP_ASSUMED_ADVERSE: 0.05,
85
+ };
86
+ export function preTradeRiskCheck(order, portfolio, limits = DEFAULT_PRE_TRADE_LIMITS, options = {}) {
87
+ const violations = [];
88
+ const volFactor = options.volFactor ?? 1.0;
89
+ // Caller-provided equity is authoritative; fall back to local compute for
90
+ // legacy test callers that don't supply it.
91
+ const currentEquity = portfolio.equity ?? computePortfolioEquity(portfolio);
92
+ // Drawdown anchor: prefer sessionStartNav, fall back to current equity.
93
+ const nav = portfolio.sessionStartNav > 0 ? portfolio.sessionStartNav : currentEquity;
94
+ const drawdownRatio = nav > 0 ? (currentEquity - nav) / nav : 0;
95
+ const zone = getDrawdownZone(drawdownRatio);
96
+ // Classify the order against any same-symbol position BEFORE the zone gates:
97
+ // - pure reduce / full close → "closing": exempt from entry gates (exits
98
+ // must always work, even in RED/ORANGE).
99
+ // - flip (over-close) → closes the old leg AND opens a NEW opposite
100
+ // leg; the NEW leg must clear every entry gate (drawdown zone, size,
101
+ // gross, per-trade-loss, bracket requirement). Previously a flip took the
102
+ // exit exemption and opened unchecked, possibly oversized, stop-less
103
+ // exposure — even in the RED zone (M6).
104
+ // - same-direction add (scale-in) / fresh entry → full entry checks (M5).
105
+ // Emergency flatten bypasses this function entirely (EmergencyControls → adapter.createOrder).
106
+ const closingPos = portfolio.positions.find(p => p.symbol === order.symbol &&
107
+ ((p.side === 'long' && order.side === 'sell') || (p.side === 'short' && order.side === 'buy')));
108
+ const flipAmount = closingPos ? order.amount - closingPos.quantity : 0;
109
+ const isFlip = closingPos != null && flipAmount > 1e-9 * Math.max(closingPos.quantity, 1);
110
+ const closing = closingPos != null && !isFlip; // reduce/close, opens no new exposure
111
+ // The leg that opens new exposure and must clear the entry gates. For a flip
112
+ // it's ONLY the excess beyond the closed position; otherwise the whole order.
113
+ // stopPrice/targetPrice carry over so the per-trade-loss and live
114
+ // bracket-requirement gates evaluate against the new leg's real stop.
115
+ const entryOrder = isFlip ? { ...order, amount: flipAmount } : order;
116
+ // Bracket requirement gates — only apply to entries (not closes) when the
117
+ // caller explicitly passes `bracketEnforcement`. Closes are exempt so
118
+ // operators can always exit, even if the entry broke the policy.
119
+ if (!closing && options.bracketEnforcement) {
120
+ const { requireStopLoss, requireTakeProfit } = options.bracketEnforcement;
121
+ if (requireStopLoss && (order.stopPrice === undefined || !(order.stopPrice > 0))) {
122
+ violations.push({
123
+ rule: 'requireStopLoss',
124
+ message: 'Live trades require a stopPrice. Disable requireStopLoss in Trading Parameters to override (not recommended).',
125
+ current: 0,
126
+ limit: 1,
127
+ });
128
+ }
129
+ if (requireTakeProfit && (order.targetPrice === undefined || !(order.targetPrice > 0))) {
130
+ violations.push({
131
+ rule: 'requireTakeProfit',
132
+ message: 'Live trades require a target_price. Disable requireTakeProfit in Trading Parameters to override.',
133
+ current: 0,
134
+ limit: 1,
135
+ });
136
+ }
137
+ if (violations.length > 0) {
138
+ return {
139
+ allowed: false,
140
+ violations,
141
+ drawdownZone: zone,
142
+ effectiveMaxPositionSize: 0,
143
+ };
144
+ }
145
+ }
146
+ // Exits are always allowed regardless of drawdown zone
147
+ if (closing) {
148
+ const effectiveMax = computeEffectivePositionSize(limits.maxPositionSize, volFactor, zone);
149
+ return {
150
+ allowed: true,
151
+ violations: [],
152
+ drawdownZone: zone,
153
+ effectiveMaxPositionSize: effectiveMax,
154
+ };
155
+ }
156
+ // RED zone: reject new entries (exits already handled above)
157
+ if (zone === 'RED') {
158
+ violations.push({
159
+ rule: 'drawdownZone',
160
+ message: `RED zone (${(drawdownRatio * 100).toFixed(2)}% drawdown) — new entries rejected`,
161
+ current: drawdownRatio,
162
+ limit: DRAWDOWN_ZONE_THRESHOLDS.RED,
163
+ });
164
+ return {
165
+ allowed: false,
166
+ violations,
167
+ drawdownZone: zone,
168
+ effectiveMaxPositionSize: 0,
169
+ };
170
+ }
171
+ // ORANGE zone: only exits allowed (already handled above)
172
+ if (zone === 'ORANGE') {
173
+ violations.push({
174
+ rule: 'drawdownZone',
175
+ message: `ORANGE zone (${(drawdownRatio * 100).toFixed(2)}% drawdown) — new entries blocked, exits only`,
176
+ current: drawdownRatio,
177
+ limit: DRAWDOWN_ZONE_THRESHOLDS.ORANGE,
178
+ });
179
+ return {
180
+ allowed: false,
181
+ violations,
182
+ drawdownZone: zone,
183
+ effectiveMaxPositionSize: 0,
184
+ };
185
+ }
186
+ // ---- Entry checks (GREEN/YELLOW) ----
187
+ const streak = options.consecutiveLosses ?? 0;
188
+ const tp = getCachedTradingParams();
189
+ // Equity-proportional floor — shrinks the static cap on small accounts.
190
+ // A $10k default `maxPositionSize` is useless on a $289 account; cap to
191
+ // `EQUITY_FLOOR.MAX_POSITION_PCT` of current equity. The tighter of the two wins.
192
+ const equityPositionCap = currentEquity > 0
193
+ ? currentEquity * EQUITY_FLOOR.MAX_POSITION_PCT
194
+ : limits.maxPositionSize;
195
+ const scaledMaxPositionSize = Math.min(limits.maxPositionSize, equityPositionCap);
196
+ let effectiveMaxPositionSize = computeEffectivePositionSize(scaledMaxPositionSize, volFactor, zone);
197
+ // Graduated size reduction on loss streaks (never a hard block — that creates deadlocks)
198
+ // Ensure correct ordering: the harsher reduction (0.25x) needs the higher threshold
199
+ const quarterThreshold = Math.max(tp.lossStreakQuarterSize, tp.lossStreakHalfSize);
200
+ const halfThreshold = Math.min(tp.lossStreakQuarterSize, tp.lossStreakHalfSize);
201
+ if (streak >= quarterThreshold) {
202
+ effectiveMaxPositionSize *= 0.25;
203
+ }
204
+ else if (streak >= halfThreshold) {
205
+ effectiveMaxPositionSize *= 0.5;
206
+ }
207
+ // 1. Position size check — against the RESULTING position notional, not just
208
+ // this order's increment. A same-direction scale-in must include the
209
+ // existing leg, or repeated small adds each pass individually yet build a
210
+ // position far beyond the per-symbol cap (M5). A flip's resulting position
211
+ // is only the new opposite leg (the old leg is closed), so the existing
212
+ // notional is NOT added there.
213
+ const entryNotional = entryOrder.amount * order.price;
214
+ const sameSymbolPos = portfolio.positions.find(p => p.symbol === order.symbol);
215
+ const isScaleIn = sameSymbolPos != null &&
216
+ ((sameSymbolPos.side === 'long' && order.side === 'buy') ||
217
+ (sameSymbolPos.side === 'short' && order.side === 'sell'));
218
+ const existingNotional = isScaleIn && sameSymbolPos
219
+ ? sameSymbolPos.quantity * (sameSymbolPos.markPrice ?? sameSymbolPos.entryPrice)
220
+ : 0;
221
+ const resultingNotional = existingNotional + entryNotional;
222
+ if (resultingNotional > effectiveMaxPositionSize) {
223
+ violations.push({
224
+ rule: 'positionSize',
225
+ message: `Resulting position notional $${resultingNotional.toFixed(2)} (existing $${existingNotional.toFixed(2)} + order $${entryNotional.toFixed(2)}) exceeds effective max $${effectiveMaxPositionSize.toFixed(2)} (equity $${currentEquity.toFixed(2)}, cap ${(EQUITY_FLOOR.MAX_POSITION_PCT * 100).toFixed(0)}% of equity)`,
226
+ current: resultingNotional,
227
+ limit: effectiveMaxPositionSize,
228
+ });
229
+ }
230
+ // 2. Open positions count (would a new symbol exceed limit?)
231
+ const existingSymbols = new Set(portfolio.positions.map(p => p.symbol));
232
+ if (!existingSymbols.has(order.symbol)) {
233
+ const newCount = existingSymbols.size + 1;
234
+ if (newCount > limits.maxOpenPositions) {
235
+ violations.push({
236
+ rule: 'openPositions',
237
+ message: `New position would bring count to ${newCount}, max is ${limits.maxOpenPositions}`,
238
+ current: newCount,
239
+ limit: limits.maxOpenPositions,
240
+ });
241
+ }
242
+ }
243
+ // 3. Projected gross exposure. On a flip the closed leg leaves the book, so
244
+ // net it out — otherwise the projection double-counts the position we close.
245
+ if (nav > 0) {
246
+ const currentGross = computeGrossExposure(portfolio);
247
+ const closedLegNotional = isFlip && closingPos
248
+ ? closingPos.quantity * (closingPos.markPrice ?? closingPos.entryPrice)
249
+ : 0;
250
+ const projectedGross = currentGross - closedLegNotional + entryNotional;
251
+ const projectedRatio = projectedGross / nav;
252
+ const effectiveMaxGross = adjustLimitByVol(limits.maxGrossExposure, volFactor);
253
+ if (projectedRatio > effectiveMaxGross) {
254
+ violations.push({
255
+ rule: 'grossExposure',
256
+ message: `Projected gross exposure ${projectedRatio.toFixed(2)}x exceeds limit ${effectiveMaxGross.toFixed(2)}x`,
257
+ current: projectedRatio,
258
+ limit: effectiveMaxGross,
259
+ });
260
+ }
261
+ }
262
+ // 4. Per-trade loss check — MANDATORY (was previously opt-in on stopPrice).
263
+ // Without a stop, we presume a 5% adverse move as the worst-case loss. This
264
+ // prevents the gate from waving through stopless entries on thin accounts.
265
+ if (!closing) {
266
+ // Use the opening leg's amount/notional (the flip's new leg, or the whole
267
+ // order for a fresh entry/scale-in) so the flip's new exposure is gated.
268
+ const expectedLoss = order.stopPrice != null
269
+ ? Math.abs(order.price - order.stopPrice) * entryOrder.amount
270
+ : entryNotional * EQUITY_FLOOR.NO_STOP_ASSUMED_ADVERSE;
271
+ const equityLossCap = currentEquity > 0
272
+ ? currentEquity * EQUITY_FLOOR.MAX_PER_TRADE_LOSS_PCT
273
+ : limits.maxPerTradeLoss;
274
+ const scaledPerTradeLoss = Math.min(limits.maxPerTradeLoss, equityLossCap);
275
+ if (expectedLoss > scaledPerTradeLoss) {
276
+ const stopNote = order.stopPrice != null
277
+ ? `stop @ ${order.stopPrice}`
278
+ : `no stop — assumed ${(EQUITY_FLOOR.NO_STOP_ASSUMED_ADVERSE * 100).toFixed(0)}% adverse move`;
279
+ violations.push({
280
+ rule: 'perTradeLoss',
281
+ message: `Expected loss $${expectedLoss.toFixed(2)} (${stopNote}) exceeds per-trade limit $${scaledPerTradeLoss.toFixed(2)} (${(EQUITY_FLOOR.MAX_PER_TRADE_LOSS_PCT * 100).toFixed(0)}% of equity $${currentEquity.toFixed(2)})`,
282
+ current: expectedLoss,
283
+ limit: scaledPerTradeLoss,
284
+ });
285
+ }
286
+ }
287
+ return {
288
+ allowed: violations.length === 0,
289
+ violations,
290
+ drawdownZone: zone,
291
+ effectiveMaxPositionSize,
292
+ };
293
+ }
294
+ // ---- Exported helpers ----
295
+ /** Count consecutive losses from the end of a trade history array. */
296
+ export function computeConsecutiveLosses(trades) {
297
+ let streak = 0;
298
+ for (let i = trades.length - 1; i >= 0; i--) {
299
+ if (trades[i].realizedPnl < 0) {
300
+ streak++;
301
+ }
302
+ else {
303
+ break;
304
+ }
305
+ }
306
+ return streak;
307
+ }
308
+ // ---- Internal helpers ----
309
+ function computeEffectivePositionSize(baseMax, volFactor, zone) {
310
+ let effective = adjustLimitByVol(baseMax, volFactor);
311
+ if (zone === 'YELLOW') {
312
+ effective *= 0.5; // YELLOW halves position sizes
313
+ }
314
+ return effective;
315
+ }
316
+ function computePortfolioEquity(portfolio) {
317
+ let equity = portfolio.walletTotal;
318
+ for (const pos of portfolio.positions) {
319
+ const mark = pos.markPrice ?? pos.entryPrice;
320
+ const pnl = pos.side === 'long'
321
+ ? (mark - pos.entryPrice) * pos.quantity
322
+ : (pos.entryPrice - mark) * pos.quantity;
323
+ if (portfolio.isLive) {
324
+ // Live Binance: wallet already includes margin — add only unrealized P&L.
325
+ equity += pnl;
326
+ }
327
+ else {
328
+ // Paper simulator (SPOT-collateral model): the full entry notional was
329
+ // deducted from wallet at open and is released on close, so we must add
330
+ // it back here. Same formula for longs and shorts — the previous
331
+ // `-qty*mark` short branch was arithmetically wrong and underrepresented
332
+ // equity by 2*entry*qty whenever the account held a short.
333
+ equity += pos.entryPrice * pos.quantity + pnl;
334
+ }
335
+ }
336
+ return equity;
337
+ }
338
+ function computeGrossExposure(portfolio) {
339
+ let gross = 0;
340
+ for (const pos of portfolio.positions) {
341
+ const price = pos.markPrice ?? pos.entryPrice;
342
+ gross += Math.abs(pos.quantity * price);
343
+ }
344
+ return gross;
345
+ }
@@ -0,0 +1,60 @@
1
+ export type DrawdownZone = 'GREEN' | 'YELLOW' | 'ORANGE' | 'RED';
2
+ export interface PreTradeRiskLimits {
3
+ maxPositionSize: number;
4
+ maxOpenPositions: number;
5
+ maxGrossExposure: number;
6
+ maxPerTradeLoss: number;
7
+ }
8
+ export interface ProposedOrder {
9
+ symbol: string;
10
+ side: 'buy' | 'sell';
11
+ type: 'market' | 'limit';
12
+ amount: number;
13
+ price: number;
14
+ stopPrice?: number;
15
+ targetPrice?: number;
16
+ }
17
+ export interface PortfolioSnapshot {
18
+ positions: Array<{
19
+ symbol: string;
20
+ side: 'long' | 'short';
21
+ quantity: number;
22
+ entryPrice: number;
23
+ markPrice?: number;
24
+ }>;
25
+ walletTotal: number;
26
+ walletAvailable: number;
27
+ sessionStartNav: number;
28
+ /** Mark-to-market total account value. If provided, takes precedence over
29
+ * the gate's own equity computation — the caller's value is authoritative
30
+ * because it has access to the real simulator/exchange state. When absent
31
+ * (legacy test callers), the gate falls back to an internal computation. */
32
+ equity?: number;
33
+ /** Paper simulator deducts notional from wallet at entry — equity = wallet + position value.
34
+ * Live Binance wallet already includes margin — equity = wallet + unrealized PnL only. */
35
+ isLive?: boolean;
36
+ }
37
+ export interface PreTradeViolation {
38
+ rule: string;
39
+ message: string;
40
+ current: number;
41
+ limit: number;
42
+ }
43
+ export interface PreTradeResult {
44
+ allowed: boolean;
45
+ violations: PreTradeViolation[];
46
+ drawdownZone: DrawdownZone;
47
+ effectiveMaxPositionSize: number;
48
+ }
49
+ export interface PreTradeOptions {
50
+ volFactor?: number;
51
+ consecutiveLosses?: number;
52
+ /** When true, the gate rejects entries without a stopPrice and/or targetPrice
53
+ * based on the flags below. Only meaningful in live mode with bracket orders
54
+ * enabled — in paper/off mode the stop-watcher handles protection and these
55
+ * flags don't apply (gate ignores them). */
56
+ bracketEnforcement?: {
57
+ requireStopLoss: boolean;
58
+ requireTakeProfit: boolean;
59
+ };
60
+ }
@@ -0,0 +1,3 @@
1
+ // Pre-trade risk gate types.
2
+ // Used by preTradeRiskCheck() to validate orders before they reach the simulator.
3
+ export {};