@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,684 @@
1
+ // In-memory exchange simulator.
2
+ // Maintains wallet, positions, open orders, and trade history.
3
+ // Emits 'stateChanged' event for persistence.
4
+ //
5
+ // Phase 9a: Supports realistic fills (book-aware VWAP, latency, maker/taker fees)
6
+ // via SimulationConfig and OrderBookDepth.
7
+ import { EventEmitter } from 'node:events';
8
+ import { randomUUID } from 'node:crypto';
9
+ import { logger } from '../logger.js';
10
+ import { MAX_TRADE_HISTORY, DEFAULT_SIMULATION_CONFIG } from './types.js';
11
+ import { fillMarketOrder, fillLimitOrder, parseSymbol } from './fill-engine.js';
12
+ import { updateMfe } from '../mfe.js';
13
+ import { computeInvalidationHit } from '../pinned-plan.js';
14
+ const TAG = 'simulator';
15
+ export class ExchangeSimulator extends EventEmitter {
16
+ state;
17
+ lastTicker = new Map();
18
+ lastOrderBook = new Map();
19
+ simulationConfig;
20
+ /** Metadata for pending limit orders, keyed by order ID. Cleaned up on fill/cancel. */
21
+ pendingOrderMetadata = new Map();
22
+ // ---- Volatility caching (set by plugin after market structure fetch) ----
23
+ cachedVolFactor = 1.0;
24
+ baselineAtr = 0;
25
+ atrSampleCount = 0;
26
+ // ---- Startup trade lockout ----
27
+ // Prevents stale agent sessions from executing trades during gateway restart.
28
+ // Without this, the agent resumes mid-action and sells positions it doesn't remember.
29
+ startupTime = Date.now();
30
+ hadPositionsAtStartup;
31
+ static STARTUP_LOCKOUT_MS = 15_000; // 15 seconds
32
+ constructor(initialState, config) {
33
+ super();
34
+ this.state = initialState;
35
+ this.simulationConfig = config ?? DEFAULT_SIMULATION_CONFIG;
36
+ this.hadPositionsAtStartup = initialState.positions.length > 0;
37
+ if (this.hadPositionsAtStartup) {
38
+ logger.info(TAG, `Startup lockout ACTIVE for 15s — ${initialState.positions.length} existing position(s) protected`);
39
+ }
40
+ logger.info(TAG, `Simulator initialized: ${this.state.positions.length} positions, ${this.state.openOrders.length} open orders (realistic fills enabled)`);
41
+ }
42
+ // ---- Simulation config ----
43
+ getSimulationConfig() {
44
+ return this.simulationConfig;
45
+ }
46
+ // ---- Volatility factor accessors ----
47
+ setVolFactor(factor) {
48
+ this.cachedVolFactor = factor;
49
+ }
50
+ getVolFactor() {
51
+ return this.cachedVolFactor;
52
+ }
53
+ /** Update baseline ATR using exponential moving average of samples. */
54
+ updateBaselineAtr(atr14) {
55
+ if (atr14 <= 0)
56
+ return;
57
+ this.atrSampleCount++;
58
+ if (this.baselineAtr === 0) {
59
+ this.baselineAtr = atr14;
60
+ }
61
+ else {
62
+ // EMA with alpha = 2/(n+1), capped at 20 samples for stability
63
+ const n = Math.min(this.atrSampleCount, 20);
64
+ const alpha = 2 / (n + 1);
65
+ this.baselineAtr = alpha * atr14 + (1 - alpha) * this.baselineAtr;
66
+ }
67
+ }
68
+ getBaselineAtr() {
69
+ return this.baselineAtr;
70
+ }
71
+ // ---- Session start NAV ----
72
+ /**
73
+ * Returns today's session-start NAV, lazily seeding it from current equity
74
+ * if unset or stale (different UTC day). This is the plugin-side anchor used
75
+ * by the pre-trade risk gate; a stale/missing anchor silently disables the
76
+ * drawdown zones, so callers must never see a zero.
77
+ */
78
+ getSessionStartNav() {
79
+ this.ensureSessionAnchor();
80
+ return this.state.sessionStartNav ?? 0;
81
+ }
82
+ setSessionStartNav(nav) {
83
+ const today = new Date().toISOString().slice(0, 10);
84
+ if (this.state.sessionDate !== today || this.state.sessionStartNav === undefined) {
85
+ this.state.sessionStartNav = nav;
86
+ this.state.sessionDate = today;
87
+ this.emitStateChanged();
88
+ }
89
+ }
90
+ /** Seed sessionStartNav from current equity on the first check of each UTC day.
91
+ * Safe to call repeatedly — no-op once the anchor matches today's date. */
92
+ ensureSessionAnchor() {
93
+ const today = new Date().toISOString().slice(0, 10);
94
+ if (this.state.sessionDate === today && this.state.sessionStartNav !== undefined) {
95
+ return;
96
+ }
97
+ const equity = this.computeEquity();
98
+ this.state.sessionStartNav = equity;
99
+ this.state.sessionDate = today;
100
+ logger.info(TAG, `Session NAV anchored: $${equity.toFixed(2)} for ${today} (was: date=${this.state.sessionDate ?? 'none'}, nav=${this.state.sessionStartNav ?? 'none'})`);
101
+ this.emitStateChanged();
102
+ }
103
+ /** Mark-to-market equity = walletTotal + for each open position, the
104
+ * collateral locked at entry + current unrealized P&L. Paper simulator is
105
+ * SPOT-collateral style: the full entry notional was deducted from wallet
106
+ * on open and is released on close, so we must add it back here. */
107
+ computeEquity() {
108
+ const quote = this.getQuoteCurrency();
109
+ const walletTotal = this.state.wallet[quote]?.total ?? 0;
110
+ let positionEquity = 0;
111
+ for (const pos of this.state.positions) {
112
+ const ticker = this.lastTicker.get(pos.symbol);
113
+ const mark = ticker?.last ?? pos.entryPrice;
114
+ const entryNotional = pos.entryPrice * pos.quantity;
115
+ const unrealized = pos.side === 'long'
116
+ ? (mark - pos.entryPrice) * pos.quantity
117
+ : (pos.entryPrice - mark) * pos.quantity;
118
+ positionEquity += entryNotional + unrealized;
119
+ }
120
+ return walletTotal + positionEquity;
121
+ }
122
+ getQuoteCurrency() {
123
+ return this.state.config?.quoteCurrency ?? 'USDT';
124
+ }
125
+ // ---- Read operations (for tools) ----
126
+ getBalance() {
127
+ const free = {};
128
+ const used = {};
129
+ const total = {};
130
+ const result = { free, used, total };
131
+ // Derivatives model: only quote currency (USDT) in wallet
132
+ for (const [currency, bal] of Object.entries(this.state.wallet)) {
133
+ free[currency] = bal.available;
134
+ used[currency] = bal.locked;
135
+ total[currency] = bal.total;
136
+ result[currency] = {
137
+ free: bal.available,
138
+ used: bal.locked,
139
+ total: bal.total,
140
+ };
141
+ }
142
+ // All-time realized P&L from trade history (restart-proof).
143
+ // Includes BOTH open-side and close-side fees — prior versions omitted the
144
+ // open-side fee and under-reported fee drag by ~50%.
145
+ let realizedPnlAllTime = 0;
146
+ let totalRoundtripFees = 0;
147
+ const today = new Date().toISOString().slice(0, 10);
148
+ let realizedPnlToday = 0;
149
+ for (const t of this.state.tradeHistory) {
150
+ const roundtripFee = t.fee + (t.openFee ?? 0);
151
+ const net = t.realizedPnl - roundtripFee;
152
+ realizedPnlAllTime += net;
153
+ totalRoundtripFees += roundtripFee;
154
+ if (typeof t.closedAt === 'string' && t.closedAt.slice(0, 10) === today) {
155
+ realizedPnlToday += net;
156
+ }
157
+ }
158
+ const round4 = (n) => +n.toFixed(4);
159
+ result.realizedPnlAllTime = round4(realizedPnlAllTime);
160
+ result.realizedPnlToday = round4(realizedPnlToday);
161
+ result.totalRoundtripFees = round4(totalRoundtripFees);
162
+ // Session execution-quality costs (since boot) for the dashboard cost cells
163
+ // (M1). These are the SAME fees/slippage already baked into equity/NAV —
164
+ // surfaced so the "Fees" / "Avg Slip" breakdown cells don't read $0 while
165
+ // the headline PnL clearly shows the drag.
166
+ const execStats = this.state.executionStats;
167
+ result.sessionFeesPaid = round4(execStats?.totalFeesPaid ?? 0);
168
+ result.sessionAvgSlippageBps = round4(execStats?.avgSlippageBps ?? 0);
169
+ // Equity = mark-to-market total account value. Use this (NOT walletTotal)
170
+ // for heat / leverage / exposure math — walletTotal drops as notional is
171
+ // locked into open positions, but that collateral is released on close.
172
+ const equity = this.computeEquity();
173
+ result.equity = round4(equity);
174
+ // Session anchor (for drawdown zone calculation). Side-effect: seeds on
175
+ // first read each UTC day if stale/missing.
176
+ const sessionNav = this.getSessionStartNav();
177
+ result.sessionStartNav = round4(sessionNav);
178
+ return result;
179
+ }
180
+ getPositions(symbol) {
181
+ const positions = symbol
182
+ ? this.state.positions.filter(p => p.symbol === symbol)
183
+ : this.state.positions;
184
+ return positions.map(p => {
185
+ const ticker = this.lastTicker.get(p.symbol);
186
+ const markPrice = ticker?.last ?? p.entryPrice;
187
+ const notional = p.quantity * markPrice;
188
+ const pnlMultiplier = p.side === 'long' ? 1 : -1;
189
+ const unrealizedPnl = (markPrice - p.entryPrice) * p.quantity * pnlMultiplier;
190
+ const percentage = p.entryPrice > 0
191
+ ? ((markPrice - p.entryPrice) / p.entryPrice) * 100 * pnlMultiplier
192
+ : 0;
193
+ return {
194
+ symbol: p.symbol,
195
+ side: p.side,
196
+ contracts: p.quantity,
197
+ contractSize: 1,
198
+ entryPrice: p.entryPrice,
199
+ markPrice,
200
+ notional,
201
+ unrealizedPnl,
202
+ percentage,
203
+ timestamp: Date.now(),
204
+ datetime: new Date().toISOString(),
205
+ // Surface entry metadata (if available)
206
+ ...(p.metadata && {
207
+ setupType: p.metadata.setupType,
208
+ thesis: p.metadata.thesis,
209
+ stopPrice: p.metadata.stopPrice,
210
+ targetPrice: p.metadata.targetPrice,
211
+ regime: p.metadata.regime,
212
+ regimeConfidence: p.metadata.regimeConfidence,
213
+ scorecardVerdict: p.metadata.scorecardVerdict,
214
+ confluenceScore: p.metadata.confluenceScore,
215
+ originalStopPrice: p.metadata.originalStopPrice,
216
+ mfePeakPrice: p.metadata.mfePeakPrice,
217
+ mfeR: p.metadata.mfeR,
218
+ giveBackRatio: p.metadata.giveBackRatio,
219
+ invalidationPrice: p.metadata.invalidationPrice,
220
+ realizationRule: p.metadata.realizationRule,
221
+ invalidationHit: computeInvalidationHit(p.side, markPrice, p.metadata.invalidationPrice),
222
+ }),
223
+ };
224
+ });
225
+ }
226
+ getOpenOrders(symbol) {
227
+ const orders = symbol
228
+ ? this.state.openOrders.filter(o => o.symbol === symbol)
229
+ : this.state.openOrders;
230
+ return orders.map(o => this.toCcxtOrder(o));
231
+ }
232
+ /** Get cumulative execution quality stats. */
233
+ getExecutionStats() {
234
+ return this.state.executionStats;
235
+ }
236
+ // ---- Order book ----
237
+ /** Cache the latest order book snapshot for a symbol. */
238
+ updateOrderBook(symbol, orderbook) {
239
+ this.lastOrderBook.set(symbol, orderbook);
240
+ }
241
+ getLastOrderBook(symbol) {
242
+ return this.lastOrderBook.get(symbol);
243
+ }
244
+ // ---- Write operations (for tools) ----
245
+ createOrder(symbol, side, type, amount, price, metadata) {
246
+ // ---- Startup trade lockout ----
247
+ // Block trades during the first 15s after gateway restart IF there were
248
+ // existing positions at startup. This prevents stale agent sessions from
249
+ // selling positions before the session is cleared and the agent re-reads SKILL.md.
250
+ // Only activates when positions exist (nothing to protect if starting empty).
251
+ const elapsed = Date.now() - this.startupTime;
252
+ if (this.hadPositionsAtStartup && elapsed < ExchangeSimulator.STARTUP_LOCKOUT_MS) {
253
+ const remaining = Math.ceil((ExchangeSimulator.STARTUP_LOCKOUT_MS - elapsed) / 1000);
254
+ logger.warn(TAG, `STARTUP LOCKOUT: Blocked ${side} ${amount} ${symbol} — ${remaining}s remaining. This prevents stale session trades during restart.`);
255
+ throw new Error(`Trade blocked: startup lockout (${remaining}s remaining). The gateway just restarted — wait for the agent to re-read its instructions and check positions before trading.`);
256
+ }
257
+ if (amount <= 0) {
258
+ throw new Error('Order amount must be positive');
259
+ }
260
+ if (type === 'limit' && (price === undefined || price <= 0)) {
261
+ throw new Error('Limit orders require a positive price');
262
+ }
263
+ const now = new Date().toISOString();
264
+ const order = {
265
+ id: randomUUID(),
266
+ symbol,
267
+ side,
268
+ type,
269
+ status: 'open',
270
+ amount,
271
+ price: type === 'limit' ? price : null,
272
+ filled: 0,
273
+ average: null,
274
+ cost: 0,
275
+ fee: { cost: 0, currency: parseSymbol(symbol).quote },
276
+ createdAt: now,
277
+ };
278
+ if (type === 'market') {
279
+ // Market orders fill immediately at current price
280
+ const ticker = this.lastTicker.get(symbol);
281
+ if (!ticker) {
282
+ throw new Error(`No ticker data for ${symbol}. Call updateTicker() first.`);
283
+ }
284
+ return this.executeMarketFill(order, ticker.last, metadata);
285
+ }
286
+ // Limit order — check if it crosses the current price
287
+ const ticker = this.lastTicker.get(symbol);
288
+ if (ticker && this.shouldFillLimit(order, ticker.last)) {
289
+ return this.executeLimitFill(order, ticker.last, metadata);
290
+ }
291
+ // Limit order doesn't cross — add to open orders. Pin the entry metadata
292
+ // ONTO the order itself (durable home, persisted in state.json) AND in the
293
+ // in-memory cache (fast path). order.metadata is the one that survives a
294
+ // restart / two-process reload, so a resting limit never fills naked.
295
+ if (metadata) {
296
+ order.metadata = metadata;
297
+ this.pendingOrderMetadata.set(order.id, metadata);
298
+ }
299
+ this.state.openOrders.push(order);
300
+ this.emitStateChanged();
301
+ logger.info(TAG, `Limit order created: ${side} ${amount} ${symbol} @ ${price}`);
302
+ return this.toCcxtOrder(order);
303
+ }
304
+ cancelOrder(orderId) {
305
+ const idx = this.state.openOrders.findIndex(o => o.id === orderId);
306
+ if (idx === -1) {
307
+ throw new Error(`Order not found: ${orderId}`);
308
+ }
309
+ const order = this.state.openOrders.splice(idx, 1)[0];
310
+ order.status = 'canceled';
311
+ this.pendingOrderMetadata.delete(orderId);
312
+ this.emitStateChanged();
313
+ logger.info(TAG, `Order cancelled: ${orderId}`);
314
+ return this.toCcxtOrder(order);
315
+ }
316
+ cancelAllOrders(symbol) {
317
+ const cancelled = [];
318
+ const remaining = [];
319
+ for (const order of this.state.openOrders) {
320
+ if (!symbol || order.symbol === symbol) {
321
+ order.status = 'canceled';
322
+ cancelled.push(order);
323
+ this.pendingOrderMetadata.delete(order.id);
324
+ }
325
+ else {
326
+ remaining.push(order);
327
+ }
328
+ }
329
+ this.state.openOrders = remaining;
330
+ if (cancelled.length > 0) {
331
+ this.emitStateChanged();
332
+ logger.info(TAG, `Cancelled ${cancelled.length} orders${symbol ? ` for ${symbol}` : ''}`);
333
+ }
334
+ return cancelled.map(o => this.toCcxtOrder(o));
335
+ }
336
+ closePosition(symbol, closeReason) {
337
+ const position = this.state.positions.find(p => p.symbol === symbol);
338
+ if (!position) {
339
+ throw new Error(`No open position for ${symbol}`);
340
+ }
341
+ // Tag the position's metadata with the close reason BEFORE firing the close
342
+ // order. The closing Trade record copies `existingPosition.metadata` in
343
+ // fill-engine.executeOrderFill, so this is what makes the reason reach
344
+ // the trade history (and therefore the agent on its next heartbeat).
345
+ if (closeReason) {
346
+ position.metadata = { ...(position.metadata ?? {}), closeReason };
347
+ }
348
+ // Create opposing market order to close the position
349
+ const closeSide = position.side === 'long' ? 'sell' : 'buy';
350
+ return this.createOrder(symbol, closeSide, 'market', position.quantity);
351
+ }
352
+ /** Paper-only: move an open position's MUTABLE protective levels (stopPrice /
353
+ * targetPrice) in place and persist, WITHOUT the close+reopen round-trip
354
+ * (which pays an extra taker fee and resets the R/MFE denominators). The
355
+ * frozen originalStopPrice / originalEntryPrice are intentionally left
356
+ * untouched so R-multiples stay anchored at entry. The paper stop-watcher
357
+ * and getPositions both read metadata.stopPrice, so a moved stop takes
358
+ * effect on the next watcher tick. Throws if there is no open position. (M9) */
359
+ updatePositionMetadata(symbol, patch) {
360
+ const pos = this.state.positions.find(p => p.symbol === symbol);
361
+ if (!pos) {
362
+ throw new Error(`No open position for ${symbol}`);
363
+ }
364
+ const meta = { ...(pos.metadata ?? {}) };
365
+ if (patch.stopPrice !== undefined)
366
+ meta.stopPrice = patch.stopPrice;
367
+ if (patch.targetPrice !== undefined)
368
+ meta.targetPrice = patch.targetPrice;
369
+ pos.metadata = meta;
370
+ this.emitStateChanged();
371
+ }
372
+ // ---- Ticker updates (checks pending limit fills) ----
373
+ updateTicker(ticker) {
374
+ this.lastTicker.set(ticker.symbol, ticker);
375
+ this.refreshMfeForSymbol(ticker.symbol, ticker.last);
376
+ // Check if any pending limit orders should fill
377
+ const toFill = [];
378
+ const remaining = [];
379
+ for (const order of this.state.openOrders) {
380
+ if (order.symbol === ticker.symbol && this.shouldFillLimit(order, ticker.last)) {
381
+ toFill.push(order);
382
+ }
383
+ else {
384
+ remaining.push(order);
385
+ }
386
+ }
387
+ if (toFill.length > 0) {
388
+ this.state.openOrders = remaining;
389
+ for (const order of toFill) {
390
+ // Prefer the durable metadata pinned on the order (survives restart);
391
+ // fall back to the in-memory cache for any legacy in-flight order.
392
+ const meta = order.metadata ?? this.pendingOrderMetadata.get(order.id);
393
+ try {
394
+ this.executeLimitFill(order, ticker.last, meta);
395
+ // Clear the cache only AFTER a successful fill so a throw-then-retry
396
+ // (e.g. transient insufficient balance) keeps the metadata for the
397
+ // next tick instead of filling naked.
398
+ this.pendingOrderMetadata.delete(order.id);
399
+ }
400
+ catch (err) {
401
+ // Fill failed (e.g. insufficient balance) — restore order to open
402
+ // list. order.metadata stays intact, so the retry is not naked.
403
+ order.status = 'open';
404
+ this.state.openOrders.push(order);
405
+ const message = err instanceof Error ? err.message : String(err);
406
+ logger.warn(TAG, `Limit fill failed for ${order.id}, order restored: ${message}`);
407
+ this.emit('fillError', {
408
+ orderId: order.id,
409
+ symbol: order.symbol,
410
+ error: message,
411
+ timestamp: new Date().toISOString(),
412
+ });
413
+ }
414
+ }
415
+ }
416
+ }
417
+ getLastTicker(symbol) {
418
+ return this.lastTicker.get(symbol);
419
+ }
420
+ /** Walk every position for `symbol` and refresh MFE / give-back from the
421
+ * latest mark. Idempotent — pure update of `metadata.mfePeakPrice` (only
422
+ * ratchets favourably) plus derived `mfeR` and `giveBackRatio`. Safe to
423
+ * call before any positions exist (no-op). */
424
+ refreshMfeForSymbol(symbol, mark) {
425
+ if (!Number.isFinite(mark) || mark <= 0)
426
+ return;
427
+ let peakAdvanced = false;
428
+ for (const p of this.state.positions) {
429
+ if (p.symbol !== symbol)
430
+ continue;
431
+ const meta = { ...(p.metadata ?? {}) };
432
+ const priorPeak = meta.mfePeakPrice;
433
+ const out = updateMfe({
434
+ side: p.side,
435
+ // R/MFE are denominated against the FROZEN original entry (and stop),
436
+ // never the running averaged entryPrice — otherwise a scale-in
437
+ // retroactively shrinks an already-achieved excursion (M4).
438
+ entryPrice: meta.originalEntryPrice ?? p.entryPrice,
439
+ originalStopPrice: meta.originalStopPrice,
440
+ markPrice: mark,
441
+ priorPeakPrice: meta.mfePeakPrice,
442
+ });
443
+ if (out.mfePeakPrice !== priorPeak)
444
+ peakAdvanced = true;
445
+ meta.mfePeakPrice = out.mfePeakPrice;
446
+ meta.mfeR = out.mfeR;
447
+ meta.giveBackRatio = out.giveBackRatio;
448
+ p.metadata = meta;
449
+ }
450
+ // Persist ONLY when a peak strictly advances (M3). The monotone peak moves
451
+ // less and less over time and debouncedSave coalesces to <=1/s, so this
452
+ // does not flood — and it is what makes the MFE ratchet survive a restart
453
+ // and a reloadState() (which previously discarded the unpersisted peak,
454
+ // feeding a stale/zeroed mfeR to the agent and the exit gate). mfeR /
455
+ // giveBackRatio are recomputed from the persisted peak on read, so they
456
+ // don't need a per-tick write.
457
+ if (peakAdvanced) {
458
+ this.emitStateChanged();
459
+ }
460
+ }
461
+ // ---- State management ----
462
+ /** Replace internal state with a fresh copy from disk.
463
+ * Used by the gateway process to pick up state saved by the agent process.
464
+ *
465
+ * Non-destructive for the MFE ratchet (M3): the disk copy can lag the live
466
+ * peak (refreshMfeForSymbol ratchets every tick; persistence coalesces), so
467
+ * a wholesale swap would discard it. For each position present in BOTH
468
+ * snapshots (same symbol+side+openedAt) we carry over the MORE-favourable
469
+ * monotone peak and recompute mfeR/giveBackRatio from the current mark, so a
470
+ * reload never regresses the agent's give-back signal. */
471
+ replaceState(newState) {
472
+ // M8: refuse a disk snapshot OLDER than what we already hold in memory.
473
+ // reloadState() reads disk that lags memory by the debounce window; without
474
+ // this guard a reload fired within ~1s of a local mutation would revert it
475
+ // (and a second mutation could then re-persist the reverted state, dropping
476
+ // a position + its realized PnL). A genuinely newer cross-process write
477
+ // (savedAt >= ours) still installs. Equal stamps install (idempotent).
478
+ const incomingAt = newState.savedAt ?? 0;
479
+ const currentAt = this.state.savedAt ?? 0;
480
+ if (incomingAt < currentAt) {
481
+ logger.debug(TAG, `Ignoring stale state reload (disk savedAt=${incomingAt} < memory savedAt=${currentAt})`);
482
+ return;
483
+ }
484
+ const priorPeakByKey = new Map();
485
+ for (const p of this.state.positions) {
486
+ if (p.metadata?.mfePeakPrice !== undefined) {
487
+ priorPeakByKey.set(`${p.symbol}|${p.side}|${p.openedAt}`, p.metadata.mfePeakPrice);
488
+ }
489
+ }
490
+ this.state = newState;
491
+ for (const p of this.state.positions) {
492
+ const priorPeak = priorPeakByKey.get(`${p.symbol}|${p.side}|${p.openedAt}`);
493
+ if (priorPeak === undefined)
494
+ continue;
495
+ const meta = { ...(p.metadata ?? {}) };
496
+ const diskPeak = meta.mfePeakPrice;
497
+ // Monotone: keep the better of disk vs in-memory (max for long, min short).
498
+ const keptPeak = diskPeak === undefined
499
+ ? priorPeak
500
+ : (p.side === 'long' ? Math.max(priorPeak, diskPeak) : Math.min(priorPeak, diskPeak));
501
+ const mark = this.lastTicker.get(p.symbol)?.last ?? meta.originalEntryPrice ?? p.entryPrice;
502
+ const out = updateMfe({
503
+ side: p.side,
504
+ entryPrice: meta.originalEntryPrice ?? p.entryPrice,
505
+ originalStopPrice: meta.originalStopPrice,
506
+ markPrice: mark,
507
+ priorPeakPrice: keptPeak,
508
+ });
509
+ meta.mfePeakPrice = out.mfePeakPrice;
510
+ meta.mfeR = out.mfeR;
511
+ meta.giveBackRatio = out.giveBackRatio;
512
+ p.metadata = meta;
513
+ }
514
+ logger.debug(TAG, `State replaced: ${newState.positions.length} positions, ${newState.openOrders.length} orders`);
515
+ }
516
+ getState() {
517
+ // Spread each element + deep-copy ONLY the nested objects that need
518
+ // isolation (metadata — which holds the nested realizationRule.scale array
519
+ // — plus executionQuality and fee). This keeps a consumer mutating the
520
+ // snapshot, or the JSON-persisted copy, from corrupting live state, while
521
+ // staying cheap on the hot persistence path: elements without metadata
522
+ // (the common case) pay only a shallow spread, never a full structuredClone.
523
+ return {
524
+ ...this.state,
525
+ wallet: Object.fromEntries(Object.entries(this.state.wallet).map(([k, v]) => [k, { ...v }])),
526
+ // Positions + open orders ARE mutated in place (updatePositionMetadata,
527
+ // refreshMfeForSymbol, scale-in, limit metadata), so deep-copy their
528
+ // nested metadata to keep a snapshot consumer from corrupting live state.
529
+ positions: this.state.positions.map(p => ({
530
+ ...p,
531
+ metadata: p.metadata ? structuredClone(p.metadata) : undefined,
532
+ })),
533
+ openOrders: this.state.openOrders.map(o => ({
534
+ ...o,
535
+ fee: { ...o.fee },
536
+ metadata: o.metadata ? structuredClone(o.metadata) : undefined,
537
+ })),
538
+ // Trade records are append-only + immutable after close, so a shallow copy
539
+ // is safe and keeps getState() O(n)-cheap on the hot persistence path even
540
+ // with a full 1000-entry history (JSON persistence still deep-serializes).
541
+ tradeHistory: this.state.tradeHistory.map(t => ({ ...t })),
542
+ executionStats: this.state.executionStats ? { ...this.state.executionStats } : undefined,
543
+ };
544
+ }
545
+ // ---- Private helpers ----
546
+ shouldFillLimit(order, currentPrice) {
547
+ if (order.price === null)
548
+ return false;
549
+ // BUY limit fills when price <= limit price
550
+ // SELL limit fills when price >= limit price
551
+ return order.side === 'buy'
552
+ ? currentPrice <= order.price
553
+ : currentPrice >= order.price;
554
+ }
555
+ executeMarketFill(order, currentPrice, metadata) {
556
+ const position = this.state.positions.find(p => p.symbol === order.symbol) ?? null;
557
+ const orderbook = this.lastOrderBook.get(order.symbol) ?? null;
558
+ const realistic = {
559
+ orderbook,
560
+ config: this.simulationConfig,
561
+ volFactor: this.cachedVolFactor,
562
+ metadata,
563
+ };
564
+ const result = fillMarketOrder(order, currentPrice, this.state.wallet, position, realistic);
565
+ const ccxtOrder = this.applyFillResult(result);
566
+ const eq = result.executionQuality;
567
+ if (eq) {
568
+ logger.info(TAG, `Market order filled: ${order.side} ${order.amount} ${order.symbol} @ ${result.order.average}` +
569
+ ` (decision: ${eq.decisionPrice.toFixed(2)}, slippage: ${eq.slippageBps.toFixed(2)}bps` +
570
+ `, latency: ${eq.latencyMs.toFixed(0)}ms, fee: ${eq.feeRate * 100}%` +
571
+ `, book: ${eq.bookDepthAvailable ? `${eq.bookLevelsConsumed} levels` : 'unavailable'})`);
572
+ }
573
+ else {
574
+ logger.info(TAG, `Market order filled: ${order.side} ${order.amount} ${order.symbol} @ ${result.order.average}`);
575
+ }
576
+ return ccxtOrder;
577
+ }
578
+ executeLimitFill(order, decisionPrice, metadata) {
579
+ const position = this.state.positions.find(p => p.symbol === order.symbol) ?? null;
580
+ const result = fillLimitOrder(order, this.state.wallet, position, this.simulationConfig, decisionPrice, metadata);
581
+ const ccxtOrder = this.applyFillResult(result);
582
+ const eq = result.executionQuality;
583
+ if (eq) {
584
+ logger.info(TAG, `Limit order filled: ${order.side} ${order.amount} ${order.symbol} @ ${result.order.average}` +
585
+ ` (maker fee: ${eq.feeRate * 100}%)`);
586
+ }
587
+ else {
588
+ logger.info(TAG, `Limit order filled: ${order.side} ${order.amount} ${order.symbol} @ ${result.order.average}`);
589
+ }
590
+ return ccxtOrder;
591
+ }
592
+ /** Apply a fill result: update positions, record trade, track stats, emit state change. */
593
+ applyFillResult(result) {
594
+ this.updatePosition(result.order.symbol, result.position);
595
+ if (result.trade)
596
+ this.addTrade(result.trade);
597
+ if (result.executionQuality)
598
+ this.updateExecutionStats(result.executionQuality);
599
+ this.emitStateChanged();
600
+ // Emit fill event for shadow tracker (Phase 9b)
601
+ this.emit('fill', {
602
+ trade: result.trade,
603
+ orderId: result.order.id,
604
+ symbol: result.order.symbol,
605
+ side: result.order.side,
606
+ type: result.order.type,
607
+ amount: result.order.amount,
608
+ fillPrice: result.order.average ?? 0,
609
+ fee: result.order.fee.cost,
610
+ });
611
+ return this.toCcxtOrder(result.order);
612
+ }
613
+ updatePosition(symbol, newPosition) {
614
+ const idx = this.state.positions.findIndex(p => p.symbol === symbol);
615
+ if (newPosition) {
616
+ if (idx >= 0) {
617
+ this.state.positions[idx] = newPosition;
618
+ }
619
+ else {
620
+ this.state.positions.push(newPosition);
621
+ }
622
+ }
623
+ else if (idx >= 0) {
624
+ this.state.positions.splice(idx, 1);
625
+ }
626
+ }
627
+ addTrade(trade) {
628
+ this.state.tradeHistory.push(trade);
629
+ // Keep only the last MAX_TRADE_HISTORY trades
630
+ if (this.state.tradeHistory.length > MAX_TRADE_HISTORY) {
631
+ this.state.tradeHistory = this.state.tradeHistory.slice(-MAX_TRADE_HISTORY);
632
+ }
633
+ }
634
+ /** Update cumulative execution stats from a fill's execution quality. */
635
+ updateExecutionStats(eq) {
636
+ if (!this.state.executionStats) {
637
+ this.state.executionStats = {
638
+ totalTrades: 0,
639
+ totalSlippageBps: 0,
640
+ totalFeesPaid: 0,
641
+ worstSlippageBps: 0,
642
+ avgSlippageBps: 0,
643
+ avgLatencyMs: 0,
644
+ };
645
+ }
646
+ const stats = this.state.executionStats;
647
+ stats.totalTrades++;
648
+ const absSlippage = Math.abs(eq.slippageBps);
649
+ stats.totalSlippageBps += absSlippage;
650
+ stats.totalFeesPaid += eq.feePaid;
651
+ if (absSlippage > stats.worstSlippageBps) {
652
+ stats.worstSlippageBps = absSlippage;
653
+ }
654
+ stats.avgSlippageBps = stats.totalSlippageBps / stats.totalTrades;
655
+ stats.avgLatencyMs =
656
+ (stats.avgLatencyMs * (stats.totalTrades - 1) + eq.latencyMs) / stats.totalTrades;
657
+ }
658
+ emitStateChanged() {
659
+ // Stamp the mutation time so a stale-disk reloadState() can be detected and
660
+ // dropped (M8). Must be set BEFORE getState() snapshots the state.
661
+ this.state.savedAt = Date.now();
662
+ this.emit('stateChanged', this.getState());
663
+ }
664
+ toCcxtOrder(order) {
665
+ const ts = new Date(order.createdAt).getTime();
666
+ return {
667
+ id: order.id,
668
+ symbol: order.symbol,
669
+ side: order.side,
670
+ type: order.type,
671
+ status: order.status,
672
+ amount: order.amount,
673
+ filled: order.filled,
674
+ remaining: order.amount - order.filled,
675
+ average: order.average,
676
+ price: order.price,
677
+ cost: order.cost,
678
+ fee: order.fee,
679
+ timestamp: ts,
680
+ datetime: order.createdAt,
681
+ timeInForce: 'GTC',
682
+ };
683
+ }
684
+ }