opencandle 0.5.0 → 0.6.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 (527) hide show
  1. package/README.md +164 -187
  2. package/dist/analysts/contracts.d.ts +1 -3
  3. package/dist/analysts/contracts.js +1 -11
  4. package/dist/analysts/contracts.js.map +1 -1
  5. package/dist/analysts/orchestrator.d.ts +1 -3
  6. package/dist/analysts/orchestrator.js +1 -26
  7. package/dist/analysts/orchestrator.js.map +1 -1
  8. package/dist/cli.js +30 -7
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +3 -3
  11. package/dist/config.js +12 -5
  12. package/dist/config.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/infra/browser.js +3 -1
  17. package/dist/infra/browser.js.map +1 -1
  18. package/dist/infra/cache.d.ts +8 -11
  19. package/dist/infra/cache.js +17 -15
  20. package/dist/infra/cache.js.map +1 -1
  21. package/dist/infra/http-client.d.ts +4 -1
  22. package/dist/infra/http-client.js +59 -6
  23. package/dist/infra/http-client.js.map +1 -1
  24. package/dist/infra/index.d.ts +3 -3
  25. package/dist/infra/index.js +3 -3
  26. package/dist/infra/index.js.map +1 -1
  27. package/dist/infra/native-dependencies.js +2 -2
  28. package/dist/infra/native-dependencies.js.map +1 -1
  29. package/dist/infra/node-version.js.map +1 -1
  30. package/dist/infra/opencandle-paths.d.ts +0 -3
  31. package/dist/infra/opencandle-paths.js +4 -11
  32. package/dist/infra/opencandle-paths.js.map +1 -1
  33. package/dist/infra/rate-limiter.js +12 -9
  34. package/dist/infra/rate-limiter.js.map +1 -1
  35. package/dist/market-state/alert-conditions.d.ts +34 -0
  36. package/dist/market-state/alert-conditions.js +23 -0
  37. package/dist/market-state/alert-conditions.js.map +1 -0
  38. package/dist/market-state/alert-runner.d.ts +55 -0
  39. package/dist/market-state/alert-runner.js +634 -0
  40. package/dist/market-state/alert-runner.js.map +1 -0
  41. package/dist/market-state/daily-report.d.ts +26 -0
  42. package/dist/market-state/daily-report.js +179 -0
  43. package/dist/market-state/daily-report.js.map +1 -0
  44. package/dist/market-state/local-automation-service.d.ts +25 -0
  45. package/dist/market-state/local-automation-service.js +119 -0
  46. package/dist/market-state/local-automation-service.js.map +1 -0
  47. package/dist/market-state/notification-delivery.d.ts +14 -0
  48. package/dist/market-state/notification-delivery.js +139 -0
  49. package/dist/market-state/notification-delivery.js.map +1 -0
  50. package/dist/market-state/resolve-for-mutation.d.ts +10 -0
  51. package/dist/market-state/resolve-for-mutation.js +15 -0
  52. package/dist/market-state/resolve-for-mutation.js.map +1 -0
  53. package/dist/market-state/resolve.d.ts +14 -0
  54. package/dist/market-state/resolve.js +89 -0
  55. package/dist/market-state/resolve.js.map +1 -0
  56. package/dist/market-state/service.d.ts +527 -0
  57. package/dist/market-state/service.js +1099 -0
  58. package/dist/market-state/service.js.map +1 -0
  59. package/dist/memory/index.d.ts +7 -7
  60. package/dist/memory/index.js +6 -6
  61. package/dist/memory/index.js.map +1 -1
  62. package/dist/memory/manager.js +11 -11
  63. package/dist/memory/manager.js.map +1 -1
  64. package/dist/memory/retrieval.js +7 -4
  65. package/dist/memory/retrieval.js.map +1 -1
  66. package/dist/memory/sqlite.js +385 -3
  67. package/dist/memory/sqlite.js.map +1 -1
  68. package/dist/memory/storage.js +1 -2
  69. package/dist/memory/storage.js.map +1 -1
  70. package/dist/memory/tool-defaults.js +64 -28
  71. package/dist/memory/tool-defaults.js.map +1 -1
  72. package/dist/memory/types.js.map +1 -1
  73. package/dist/monitor.d.ts +2 -0
  74. package/dist/monitor.js +104 -0
  75. package/dist/monitor.js.map +1 -0
  76. package/dist/onboarding/connect.js +4 -6
  77. package/dist/onboarding/connect.js.map +1 -1
  78. package/dist/onboarding/credential-interceptor.js +1 -1
  79. package/dist/onboarding/credential-interceptor.js.map +1 -1
  80. package/dist/onboarding/degradation-accumulator.js +1 -3
  81. package/dist/onboarding/degradation-accumulator.js.map +1 -1
  82. package/dist/onboarding/providers.js +3 -16
  83. package/dist/onboarding/providers.js.map +1 -1
  84. package/dist/onboarding/state.js.map +1 -1
  85. package/dist/onboarding/tool-helpers.js +1 -1
  86. package/dist/onboarding/tool-helpers.js.map +1 -1
  87. package/dist/onboarding/tool-tags.js +6 -4
  88. package/dist/onboarding/tool-tags.js.map +1 -1
  89. package/dist/onboarding/validation.js +1 -1
  90. package/dist/onboarding/validation.js.map +1 -1
  91. package/dist/pi/opencandle-extension.d.ts +8 -0
  92. package/dist/pi/opencandle-extension.js +412 -28
  93. package/dist/pi/opencandle-extension.js.map +1 -1
  94. package/dist/pi/session.d.ts +1 -1
  95. package/dist/pi/session.js +3 -1
  96. package/dist/pi/session.js.map +1 -1
  97. package/dist/pi/setup.js +8 -3
  98. package/dist/pi/setup.js.map +1 -1
  99. package/dist/pi/tool-adapter.js +5 -2
  100. package/dist/pi/tool-adapter.js.map +1 -1
  101. package/dist/prompts/context-builder.d.ts +1 -1
  102. package/dist/prompts/context-builder.js +19 -6
  103. package/dist/prompts/context-builder.js.map +1 -1
  104. package/dist/prompts/policy-cards.d.ts +1 -1
  105. package/dist/prompts/policy-cards.js +1 -1
  106. package/dist/prompts/policy-cards.js.map +1 -1
  107. package/dist/prompts/sections.d.ts +1 -1
  108. package/dist/prompts/symbol-preflight.d.ts +20 -0
  109. package/dist/prompts/symbol-preflight.js +49 -0
  110. package/dist/prompts/symbol-preflight.js.map +1 -0
  111. package/dist/prompts/workflow-prompts.d.ts +1 -1
  112. package/dist/prompts/workflow-prompts.js +54 -16
  113. package/dist/prompts/workflow-prompts.js.map +1 -1
  114. package/dist/providers/alpha-vantage.d.ts +1 -1
  115. package/dist/providers/alpha-vantage.js +26 -7
  116. package/dist/providers/alpha-vantage.js.map +1 -1
  117. package/dist/providers/coingecko.js +1 -1
  118. package/dist/providers/coingecko.js.map +1 -1
  119. package/dist/providers/errors.d.ts +5 -0
  120. package/dist/providers/errors.js +11 -0
  121. package/dist/providers/errors.js.map +1 -0
  122. package/dist/providers/exa-search.d.ts +2 -2
  123. package/dist/providers/exa-search.js +19 -11
  124. package/dist/providers/exa-search.js.map +1 -1
  125. package/dist/providers/fear-greed.js +1 -1
  126. package/dist/providers/fear-greed.js.map +1 -1
  127. package/dist/providers/finnhub.js +3 -5
  128. package/dist/providers/finnhub.js.map +1 -1
  129. package/dist/providers/fred.js +2 -2
  130. package/dist/providers/fred.js.map +1 -1
  131. package/dist/providers/index.d.ts +7 -6
  132. package/dist/providers/index.js +6 -5
  133. package/dist/providers/index.js.map +1 -1
  134. package/dist/providers/reddit.js +2 -2
  135. package/dist/providers/reddit.js.map +1 -1
  136. package/dist/providers/sec-edgar.d.ts +1 -0
  137. package/dist/providers/sec-edgar.js +12 -4
  138. package/dist/providers/sec-edgar.js.map +1 -1
  139. package/dist/providers/tradingview.d.ts +47 -0
  140. package/dist/providers/tradingview.js +275 -0
  141. package/dist/providers/tradingview.js.map +1 -0
  142. package/dist/providers/twitter.js +6 -8
  143. package/dist/providers/twitter.js.map +1 -1
  144. package/dist/providers/web-search.js +26 -12
  145. package/dist/providers/web-search.js.map +1 -1
  146. package/dist/providers/with-fallback.js +4 -2
  147. package/dist/providers/with-fallback.js.map +1 -1
  148. package/dist/providers/wrap-provider.d.ts +2 -3
  149. package/dist/providers/wrap-provider.js +14 -8
  150. package/dist/providers/wrap-provider.js.map +1 -1
  151. package/dist/providers/yahoo-finance.d.ts +1 -1
  152. package/dist/providers/yahoo-finance.js +101 -17
  153. package/dist/providers/yahoo-finance.js.map +1 -1
  154. package/dist/routing/classify-intent.d.ts +6 -0
  155. package/dist/routing/classify-intent.js +78 -7
  156. package/dist/routing/classify-intent.js.map +1 -1
  157. package/dist/routing/defaults.d.ts +1 -1
  158. package/dist/routing/entity-extractor.d.ts +1 -0
  159. package/dist/routing/entity-extractor.js +234 -29
  160. package/dist/routing/entity-extractor.js.map +1 -1
  161. package/dist/routing/fund-symbols.d.ts +2 -0
  162. package/dist/routing/fund-symbols.js +55 -0
  163. package/dist/routing/fund-symbols.js.map +1 -0
  164. package/dist/routing/horizon.d.ts +1 -0
  165. package/dist/routing/horizon.js +10 -0
  166. package/dist/routing/horizon.js.map +1 -0
  167. package/dist/routing/index.d.ts +10 -10
  168. package/dist/routing/index.js +6 -6
  169. package/dist/routing/index.js.map +1 -1
  170. package/dist/routing/planning.d.ts +1 -1
  171. package/dist/routing/planning.js +65 -34
  172. package/dist/routing/planning.js.map +1 -1
  173. package/dist/routing/route-manifest.d.ts +2 -2
  174. package/dist/routing/route-manifest.js +25 -4
  175. package/dist/routing/route-manifest.js.map +1 -1
  176. package/dist/routing/router-llm-client.js.map +1 -1
  177. package/dist/routing/router-prompt.js +7 -9
  178. package/dist/routing/router-prompt.js.map +1 -1
  179. package/dist/routing/router-types.d.ts +1 -0
  180. package/dist/routing/router.js +137 -22
  181. package/dist/routing/router.js.map +1 -1
  182. package/dist/routing/slot-resolver.d.ts +1 -1
  183. package/dist/routing/slot-resolver.js +2 -4
  184. package/dist/routing/slot-resolver.js.map +1 -1
  185. package/dist/routing/symbol-disambiguator.d.ts +11 -0
  186. package/dist/routing/symbol-disambiguator.js +52 -0
  187. package/dist/routing/symbol-disambiguator.js.map +1 -0
  188. package/dist/routing/turn-context.d.ts +1 -1
  189. package/dist/routing/turn-context.js +1 -1
  190. package/dist/routing/turn-context.js.map +1 -1
  191. package/dist/routing/types.d.ts +2 -0
  192. package/dist/runtime/answer-contracts.js +36 -8
  193. package/dist/runtime/answer-contracts.js.map +1 -1
  194. package/dist/runtime/artifact-contracts.js.map +1 -1
  195. package/dist/runtime/planning-evidence.js +47 -26
  196. package/dist/runtime/planning-evidence.js.map +1 -1
  197. package/dist/runtime/prompt-step.d.ts +1 -9
  198. package/dist/runtime/prompt-step.js +0 -10
  199. package/dist/runtime/prompt-step.js.map +1 -1
  200. package/dist/runtime/run-context.d.ts +5 -2
  201. package/dist/runtime/run-context.js +8 -1
  202. package/dist/runtime/run-context.js.map +1 -1
  203. package/dist/runtime/session-coordinator.d.ts +13 -5
  204. package/dist/runtime/session-coordinator.js +160 -20
  205. package/dist/runtime/session-coordinator.js.map +1 -1
  206. package/dist/runtime/session-title.d.ts +14 -0
  207. package/dist/runtime/session-title.js +50 -0
  208. package/dist/runtime/session-title.js.map +1 -0
  209. package/dist/runtime/tool-defaults-wrapper.js +1 -3
  210. package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
  211. package/dist/runtime/validation.js.map +1 -1
  212. package/dist/runtime/workflow-events.js.map +1 -1
  213. package/dist/runtime/workflow-runner.d.ts +3 -3
  214. package/dist/runtime/workflow-runner.js +1 -1
  215. package/dist/runtime/workflow-runner.js.map +1 -1
  216. package/dist/sentiment/adapters/finnhub.d.ts +1 -1
  217. package/dist/sentiment/adapters/finnhub.js +6 -1
  218. package/dist/sentiment/adapters/finnhub.js.map +1 -1
  219. package/dist/sentiment/adapters/reddit.d.ts +2 -2
  220. package/dist/sentiment/adapters/twitter.d.ts +1 -1
  221. package/dist/sentiment/adapters/web.d.ts +1 -1
  222. package/dist/sentiment/index.d.ts +9 -11
  223. package/dist/sentiment/index.js +9 -20
  224. package/dist/sentiment/index.js.map +1 -1
  225. package/dist/sentiment/keywords.js +26 -4
  226. package/dist/sentiment/keywords.js.map +1 -1
  227. package/dist/sentiment/pipeline.d.ts +2 -2
  228. package/dist/sentiment/pipeline.js +1 -1
  229. package/dist/sentiment/pipeline.js.map +1 -1
  230. package/dist/sentiment/scorer.js +1 -1
  231. package/dist/sentiment/store.d.ts +1 -1
  232. package/dist/sentiment/store.js +1 -1
  233. package/dist/sentiment/store.js.map +1 -1
  234. package/dist/sentiment/trends.d.ts +1 -1
  235. package/dist/sentiment/trends.js.map +1 -1
  236. package/dist/sentiment/types.js.map +1 -1
  237. package/dist/system-prompt.js +3 -2
  238. package/dist/system-prompt.js.map +1 -1
  239. package/dist/tool-kit.d.ts +7 -7
  240. package/dist/tool-kit.js +4 -4
  241. package/dist/tool-kit.js.map +1 -1
  242. package/dist/tools/fundamentals/company-overview.js +11 -6
  243. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  244. package/dist/tools/fundamentals/comps.js +18 -9
  245. package/dist/tools/fundamentals/comps.js.map +1 -1
  246. package/dist/tools/fundamentals/dcf.js +23 -11
  247. package/dist/tools/fundamentals/dcf.js.map +1 -1
  248. package/dist/tools/fundamentals/earnings.js +8 -3
  249. package/dist/tools/fundamentals/earnings.js.map +1 -1
  250. package/dist/tools/fundamentals/financials.js +8 -3
  251. package/dist/tools/fundamentals/financials.js.map +1 -1
  252. package/dist/tools/fundamentals/sec-filings.js +21 -6
  253. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  254. package/dist/tools/index.d.ts +23 -19
  255. package/dist/tools/index.js +51 -39
  256. package/dist/tools/index.js.map +1 -1
  257. package/dist/tools/interaction/ask-user.js +15 -3
  258. package/dist/tools/interaction/ask-user.js.map +1 -1
  259. package/dist/tools/interaction/twitter-login.js +13 -3
  260. package/dist/tools/interaction/twitter-login.js.map +1 -1
  261. package/dist/tools/macro/fear-greed.js.map +1 -1
  262. package/dist/tools/macro/fred-data.d.ts +1 -1
  263. package/dist/tools/macro/fred-data.js +17 -6
  264. package/dist/tools/macro/fred-data.js.map +1 -1
  265. package/dist/tools/market/crypto-history.js +3 -1
  266. package/dist/tools/market/crypto-history.js.map +1 -1
  267. package/dist/tools/market/crypto-price.js +3 -1
  268. package/dist/tools/market/crypto-price.js.map +1 -1
  269. package/dist/tools/market/screen-stocks.d.ts +18 -0
  270. package/dist/tools/market/screen-stocks.js +252 -0
  271. package/dist/tools/market/screen-stocks.js.map +1 -0
  272. package/dist/tools/market/search-ticker.js +160 -8
  273. package/dist/tools/market/search-ticker.js.map +1 -1
  274. package/dist/tools/market/stock-history.d.ts +2 -2
  275. package/dist/tools/market/stock-history.js +26 -7
  276. package/dist/tools/market/stock-history.js.map +1 -1
  277. package/dist/tools/market/stock-quote.js +5 -3
  278. package/dist/tools/market/stock-quote.js.map +1 -1
  279. package/dist/tools/options/greeks.js +1 -1
  280. package/dist/tools/options/greeks.js.map +1 -1
  281. package/dist/tools/options/option-chain.js +19 -6
  282. package/dist/tools/options/option-chain.js.map +1 -1
  283. package/dist/tools/portfolio/alerts.d.ts +15 -0
  284. package/dist/tools/portfolio/alerts.js +357 -0
  285. package/dist/tools/portfolio/alerts.js.map +1 -0
  286. package/dist/tools/portfolio/correlation.d.ts +1 -1
  287. package/dist/tools/portfolio/correlation.js +33 -13
  288. package/dist/tools/portfolio/correlation.js.map +1 -1
  289. package/dist/tools/portfolio/daily-report.d.ts +8 -0
  290. package/dist/tools/portfolio/daily-report.js +83 -0
  291. package/dist/tools/portfolio/daily-report.js.map +1 -0
  292. package/dist/tools/portfolio/holdings-overlap.js +10 -3
  293. package/dist/tools/portfolio/holdings-overlap.js.map +1 -1
  294. package/dist/tools/portfolio/notifications.d.ts +7 -0
  295. package/dist/tools/portfolio/notifications.js +43 -0
  296. package/dist/tools/portfolio/notifications.js.map +1 -0
  297. package/dist/tools/portfolio/predictions.d.ts +12 -6
  298. package/dist/tools/portfolio/predictions.js +337 -87
  299. package/dist/tools/portfolio/predictions.js.map +1 -1
  300. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  301. package/dist/tools/portfolio/risk-analysis.js +45 -6
  302. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  303. package/dist/tools/portfolio/tracker.d.ts +4 -3
  304. package/dist/tools/portfolio/tracker.js +246 -101
  305. package/dist/tools/portfolio/tracker.js.map +1 -1
  306. package/dist/tools/portfolio/watchlist.d.ts +6 -4
  307. package/dist/tools/portfolio/watchlist.js +208 -108
  308. package/dist/tools/portfolio/watchlist.js.map +1 -1
  309. package/dist/tools/sentiment/reddit-sentiment.js +23 -10
  310. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  311. package/dist/tools/sentiment/sentiment-summary.js +15 -13
  312. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  313. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  314. package/dist/tools/sentiment/sentiment-trend.js +12 -2
  315. package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
  316. package/dist/tools/sentiment/twitter-sentiment.js +12 -5
  317. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  318. package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
  319. package/dist/tools/sentiment/untrusted-text.js +17 -0
  320. package/dist/tools/sentiment/untrusted-text.js.map +1 -0
  321. package/dist/tools/sentiment/web-search.js +9 -13
  322. package/dist/tools/sentiment/web-search.js.map +1 -1
  323. package/dist/tools/sentiment/web-sentiment.js +15 -3
  324. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  325. package/dist/tools/technical/backtest.d.ts +1 -1
  326. package/dist/tools/technical/backtest.js +27 -20
  327. package/dist/tools/technical/backtest.js.map +1 -1
  328. package/dist/tools/technical/indicators.js +23 -5
  329. package/dist/tools/technical/indicators.js.map +1 -1
  330. package/dist/types/index.d.ts +3 -3
  331. package/dist/types/index.js.map +1 -1
  332. package/dist/types/market.d.ts +1 -0
  333. package/dist/types/portfolio.d.ts +14 -4
  334. package/dist/workflows/compare-assets.d.ts +0 -3
  335. package/dist/workflows/compare-assets.js +20 -11
  336. package/dist/workflows/compare-assets.js.map +1 -1
  337. package/dist/workflows/index.d.ts +3 -4
  338. package/dist/workflows/index.js +3 -3
  339. package/dist/workflows/index.js.map +1 -1
  340. package/dist/workflows/options-screener.d.ts +0 -3
  341. package/dist/workflows/options-screener.js +4 -11
  342. package/dist/workflows/options-screener.js.map +1 -1
  343. package/dist/workflows/portfolio-builder.d.ts +0 -3
  344. package/dist/workflows/portfolio-builder.js +0 -8
  345. package/dist/workflows/portfolio-builder.js.map +1 -1
  346. package/gui/server/ask-user-bridge.ts +1 -1
  347. package/gui/server/automation-heartbeat.ts +97 -0
  348. package/gui/server/background-quotes.ts +97 -1
  349. package/gui/server/chat-event-adapter.ts +32 -10
  350. package/gui/server/chat-run-session.ts +16 -0
  351. package/gui/server/invoke-tool.ts +144 -1
  352. package/gui/server/live-chat-event-adapter.ts +21 -6
  353. package/gui/server/market-state-api.ts +315 -0
  354. package/gui/server/model-setup.ts +149 -2
  355. package/gui/server/private-api-access.ts +62 -0
  356. package/gui/server/projector.ts +12 -7
  357. package/gui/server/prompt-observation.ts +4 -7
  358. package/gui/server/quote-snapshot-store.ts +50 -0
  359. package/gui/server/server.ts +200 -451
  360. package/gui/server/session-actions.ts +186 -1
  361. package/gui/server/shutdown.ts +47 -0
  362. package/gui/server/tool-invoke-ack.ts +49 -0
  363. package/gui/server/tool-metadata.ts +23 -10
  364. package/gui/server/websocket.ts +13 -3
  365. package/gui/server/writer-lock.ts +6 -2
  366. package/gui/server/ws-hub.ts +292 -0
  367. package/gui/shared/chat-events.ts +16 -1
  368. package/gui/shared/event-reducer.ts +24 -6
  369. package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +1 -0
  370. package/gui/web/dist/assets/index-2KZtKBmu.css +1 -0
  371. package/gui/web/dist/assets/index-CveNgtDg.js +69 -0
  372. package/gui/web/dist/index.html +2 -2
  373. package/package.json +5 -1
  374. package/src/analysts/contracts.ts +10 -23
  375. package/src/analysts/orchestrator.ts +8 -43
  376. package/src/cli.ts +35 -12
  377. package/src/config.ts +17 -9
  378. package/src/index.ts +1 -1
  379. package/src/infra/browser.ts +3 -1
  380. package/src/infra/cache.ts +41 -30
  381. package/src/infra/http-client.ts +72 -6
  382. package/src/infra/index.ts +7 -10
  383. package/src/infra/native-dependencies.ts +8 -3
  384. package/src/infra/node-version.ts +3 -1
  385. package/src/infra/opencandle-paths.ts +3 -14
  386. package/src/infra/rate-limiter.ts +22 -19
  387. package/src/market-state/alert-conditions.ts +82 -0
  388. package/src/market-state/alert-runner.ts +863 -0
  389. package/src/market-state/daily-report.ts +247 -0
  390. package/src/market-state/local-automation-service.ts +162 -0
  391. package/src/market-state/notification-delivery.ts +158 -0
  392. package/src/market-state/resolve-for-mutation.ts +24 -0
  393. package/src/market-state/resolve.ts +112 -0
  394. package/src/market-state/service.ts +2344 -0
  395. package/src/memory/index.ts +7 -7
  396. package/src/memory/manager.ts +14 -16
  397. package/src/memory/retrieval.ts +8 -7
  398. package/src/memory/sqlite.ts +407 -6
  399. package/src/memory/storage.ts +5 -15
  400. package/src/memory/tool-defaults.ts +60 -39
  401. package/src/memory/types.ts +3 -3
  402. package/src/monitor.ts +121 -0
  403. package/src/onboarding/connect.ts +10 -33
  404. package/src/onboarding/credential-interceptor.ts +3 -15
  405. package/src/onboarding/degradation-accumulator.ts +1 -3
  406. package/src/onboarding/providers.ts +9 -40
  407. package/src/onboarding/state.ts +4 -15
  408. package/src/onboarding/tool-helpers.ts +2 -9
  409. package/src/onboarding/tool-tags.ts +6 -6
  410. package/src/onboarding/validation.ts +14 -20
  411. package/src/pi/opencandle-extension.ts +529 -85
  412. package/src/pi/session.ts +7 -5
  413. package/src/pi/setup.ts +61 -43
  414. package/src/pi/tool-adapter.ts +5 -2
  415. package/src/prompts/context-builder.ts +23 -12
  416. package/src/prompts/policy-cards.ts +2 -2
  417. package/src/prompts/sections.ts +1 -1
  418. package/src/prompts/symbol-preflight.ts +80 -0
  419. package/src/prompts/workflow-prompts.ts +77 -28
  420. package/src/providers/alpha-vantage.ts +58 -39
  421. package/src/providers/coingecko.ts +2 -5
  422. package/src/providers/errors.ts +9 -0
  423. package/src/providers/exa-search.ts +24 -22
  424. package/src/providers/fear-greed.ts +1 -1
  425. package/src/providers/finnhub.ts +7 -6
  426. package/src/providers/fred.ts +3 -3
  427. package/src/providers/index.ts +14 -6
  428. package/src/providers/reddit.ts +17 -6
  429. package/src/providers/sec-edgar.ts +20 -6
  430. package/src/providers/tradingview.ts +399 -0
  431. package/src/providers/twitter.ts +6 -8
  432. package/src/providers/web-search.ts +30 -20
  433. package/src/providers/with-fallback.ts +8 -7
  434. package/src/providers/wrap-provider.ts +15 -10
  435. package/src/providers/yahoo-finance.ts +140 -35
  436. package/src/routing/classify-intent.ts +101 -10
  437. package/src/routing/defaults.ts +1 -1
  438. package/src/routing/entity-extractor.ts +287 -38
  439. package/src/routing/fund-symbols.ts +58 -0
  440. package/src/routing/horizon.ts +7 -0
  441. package/src/routing/index.ts +48 -48
  442. package/src/routing/planning.ts +144 -53
  443. package/src/routing/route-manifest.ts +37 -15
  444. package/src/routing/router-llm-client.ts +4 -4
  445. package/src/routing/router-prompt.ts +15 -19
  446. package/src/routing/router-types.ts +2 -5
  447. package/src/routing/router.ts +251 -53
  448. package/src/routing/slot-resolver.ts +34 -11
  449. package/src/routing/symbol-disambiguator.ts +72 -0
  450. package/src/routing/turn-context.ts +6 -9
  451. package/src/routing/types.ts +2 -0
  452. package/src/runtime/answer-contracts.ts +82 -43
  453. package/src/runtime/artifact-contracts.ts +2 -1
  454. package/src/runtime/planning-evidence.ts +157 -66
  455. package/src/runtime/prompt-step.ts +1 -16
  456. package/src/runtime/run-context.ts +12 -2
  457. package/src/runtime/session-coordinator.ts +238 -63
  458. package/src/runtime/session-title.ts +60 -0
  459. package/src/runtime/tool-defaults-wrapper.ts +1 -3
  460. package/src/runtime/validation.ts +1 -4
  461. package/src/runtime/workflow-events.ts +7 -7
  462. package/src/runtime/workflow-runner.ts +5 -11
  463. package/src/sentiment/adapters/finnhub.ts +7 -2
  464. package/src/sentiment/adapters/reddit.ts +2 -2
  465. package/src/sentiment/adapters/twitter.ts +1 -1
  466. package/src/sentiment/adapters/web.ts +1 -1
  467. package/src/sentiment/index.ts +16 -26
  468. package/src/sentiment/keywords.ts +26 -4
  469. package/src/sentiment/pipeline.ts +15 -4
  470. package/src/sentiment/scorer.ts +1 -1
  471. package/src/sentiment/store.ts +2 -2
  472. package/src/sentiment/trends.ts +9 -3
  473. package/src/sentiment/types.ts +5 -4
  474. package/src/system-prompt.ts +3 -2
  475. package/src/tool-kit.ts +10 -9
  476. package/src/tools/fundamentals/company-overview.ts +19 -9
  477. package/src/tools/fundamentals/comps.ts +68 -55
  478. package/src/tools/fundamentals/dcf.ts +145 -95
  479. package/src/tools/fundamentals/earnings.ts +16 -6
  480. package/src/tools/fundamentals/financials.ts +16 -7
  481. package/src/tools/fundamentals/sec-filings.ts +37 -16
  482. package/src/tools/index.ts +51 -39
  483. package/src/tools/interaction/ask-user.ts +22 -10
  484. package/src/tools/interaction/twitter-login.ts +17 -5
  485. package/src/tools/macro/fear-greed.ts +1 -1
  486. package/src/tools/macro/fred-data.ts +58 -46
  487. package/src/tools/market/crypto-history.ts +8 -3
  488. package/src/tools/market/crypto-price.ts +6 -6
  489. package/src/tools/market/screen-stocks.ts +279 -0
  490. package/src/tools/market/search-ticker.ts +218 -17
  491. package/src/tools/market/stock-history.ts +37 -12
  492. package/src/tools/market/stock-quote.ts +10 -7
  493. package/src/tools/options/greeks.ts +5 -5
  494. package/src/tools/options/option-chain.ts +41 -17
  495. package/src/tools/portfolio/alerts.ts +457 -0
  496. package/src/tools/portfolio/correlation.ts +47 -20
  497. package/src/tools/portfolio/daily-report.ts +101 -0
  498. package/src/tools/portfolio/holdings-overlap.ts +31 -15
  499. package/src/tools/portfolio/notifications.ts +45 -0
  500. package/src/tools/portfolio/predictions.ts +406 -106
  501. package/src/tools/portfolio/risk-analysis.ts +46 -7
  502. package/src/tools/portfolio/tracker.ts +270 -109
  503. package/src/tools/portfolio/watchlist.ts +250 -121
  504. package/src/tools/sentiment/reddit-sentiment.ts +50 -24
  505. package/src/tools/sentiment/sentiment-summary.ts +62 -41
  506. package/src/tools/sentiment/sentiment-trend.ts +24 -7
  507. package/src/tools/sentiment/twitter-sentiment.ts +22 -15
  508. package/src/tools/sentiment/untrusted-text.ts +21 -0
  509. package/src/tools/sentiment/web-search.ts +21 -18
  510. package/src/tools/sentiment/web-sentiment.ts +26 -10
  511. package/src/tools/technical/backtest.ts +32 -22
  512. package/src/tools/technical/indicators.ts +39 -14
  513. package/src/types/index.ts +8 -3
  514. package/src/types/market.ts +1 -0
  515. package/src/types/portfolio.ts +14 -4
  516. package/src/types/sentiment.ts +2 -2
  517. package/src/workflows/compare-assets.ts +33 -21
  518. package/src/workflows/index.ts +3 -4
  519. package/src/workflows/options-screener.ts +27 -29
  520. package/src/workflows/portfolio-builder.ts +34 -27
  521. package/dist/workflows/types.d.ts +0 -4
  522. package/dist/workflows/types.js +0 -2
  523. package/dist/workflows/types.js.map +0 -1
  524. package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +0 -1
  525. package/gui/web/dist/assets/index-Bxt9QpLX.css +0 -1
  526. package/gui/web/dist/assets/index-CZ9DHZYy.js +0 -67
  527. package/src/workflows/types.ts +0 -4
@@ -0,0 +1,1099 @@
1
+ export class MarketStateService {
2
+ db;
3
+ constructor(db) {
4
+ this.db = db;
5
+ }
6
+ getDefaultWatchlist() {
7
+ const now = new Date().toISOString();
8
+ this.db
9
+ .prepare(`INSERT OR IGNORE INTO watchlists (name, is_default, created_at, updated_at)
10
+ SELECT 'Default', 1, ?, ?
11
+ WHERE NOT EXISTS (SELECT 1 FROM watchlists WHERE is_default = 1)`)
12
+ .run(now, now);
13
+ const row = this.db
14
+ .prepare("SELECT * FROM watchlists WHERE is_default = 1 LIMIT 1")
15
+ .get();
16
+ return mapCollection(row);
17
+ }
18
+ getDefaultPortfolio() {
19
+ const now = new Date().toISOString();
20
+ this.db
21
+ .prepare(`INSERT OR IGNORE INTO portfolios (name, base_currency, is_default, created_at, updated_at)
22
+ SELECT 'Default', 'USD', 1, ?, ?
23
+ WHERE NOT EXISTS (SELECT 1 FROM portfolios WHERE is_default = 1)`)
24
+ .run(now, now);
25
+ const row = this.db
26
+ .prepare("SELECT * FROM portfolios WHERE is_default = 1 LIMIT 1")
27
+ .get();
28
+ return {
29
+ ...mapCollection(row),
30
+ baseCurrency: row.base_currency,
31
+ };
32
+ }
33
+ findInstrumentByAlias(lookup) {
34
+ const source = normalizeSource(lookup.source);
35
+ const sourceId = normalizeNullable(lookup.sourceId);
36
+ const alias = sourceId == null
37
+ ? this.db
38
+ .prepare(`SELECT id, instrument_id FROM instrument_aliases
39
+ WHERE source = ?
40
+ AND source_symbol = ?
41
+ AND IFNULL(source_exchange, '') = IFNULL(?, '')
42
+ AND IFNULL(source_asset_type, '') = IFNULL(?, '')
43
+ LIMIT 1`)
44
+ .get(source, normalizeSourceSymbol(lookup.sourceSymbol ?? ""), normalizeExchange(lookup.sourceExchange), normalizeAssetType(lookup.sourceAssetType))
45
+ : this.db
46
+ .prepare(`SELECT id, instrument_id FROM instrument_aliases
47
+ WHERE source = ? AND source_id = ?
48
+ LIMIT 1`)
49
+ .get(source, sourceId);
50
+ if (alias == null)
51
+ return null;
52
+ const row = this.db
53
+ .prepare("SELECT * FROM instruments WHERE id = ?")
54
+ .get(alias.instrument_id);
55
+ return row == null ? null : mapInstrument(row);
56
+ }
57
+ addWatchlistItem(params) {
58
+ const tx = this.db.transaction(() => {
59
+ const watchlistId = params.watchlistId ?? this.getDefaultWatchlist().id;
60
+ const instrument = this.upsertInstrument(params.instrument);
61
+ const now = new Date().toISOString();
62
+ const existing = this.db
63
+ .prepare(`SELECT * FROM watchlist_items
64
+ WHERE watchlist_id = ? AND instrument_id = ?`)
65
+ .get(watchlistId, instrument.id);
66
+ if (existing) {
67
+ this.db
68
+ .prepare(`UPDATE watchlist_items
69
+ SET target_price = ?, stop_price = ?, price_currency = ?, thesis = ?,
70
+ notes = ?, tags_json = ?, source = ?, source_row_id = ?,
71
+ source_metadata_json = ?, updated_at = ?
72
+ WHERE id = ?`)
73
+ .run(params.targetPrice === undefined ? existing.target_price : params.targetPrice, params.stopPrice === undefined ? existing.stop_price : params.stopPrice, params.priceCurrency === undefined ? existing.price_currency : params.priceCurrency, params.thesis === undefined ? existing.thesis : params.thesis, params.notes === undefined ? existing.notes : params.notes, params.tags == null ? existing.tags_json : JSON.stringify(params.tags), params.source === undefined ? existing.source : normalizeNullable(params.source), params.sourceRowId === undefined
74
+ ? existing.source_row_id
75
+ : normalizeNullable(params.sourceRowId), params.sourceMetadata === undefined
76
+ ? existing.source_metadata_json
77
+ : params.sourceMetadata == null
78
+ ? null
79
+ : JSON.stringify(params.sourceMetadata), now, existing.id);
80
+ return existing.id;
81
+ }
82
+ const result = this.db
83
+ .prepare(`INSERT INTO watchlist_items (
84
+ watchlist_id, instrument_id, thesis, notes, tags_json,
85
+ target_price, stop_price, price_currency, source, source_row_id,
86
+ source_metadata_json, created_at, updated_at
87
+ )
88
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
89
+ .run(watchlistId, instrument.id, params.thesis ?? null, params.notes ?? null, params.tags == null ? null : JSON.stringify(params.tags), params.targetPrice ?? null, params.stopPrice ?? null, params.priceCurrency ?? params.instrument.currency ?? null, normalizeNullable(params.source), normalizeNullable(params.sourceRowId), params.sourceMetadata == null ? null : JSON.stringify(params.sourceMetadata), now, now);
90
+ return Number(result.lastInsertRowid);
91
+ });
92
+ return this.getWatchlistItem(tx());
93
+ }
94
+ listWatchlistItems(watchlistId = this.getDefaultWatchlist().id) {
95
+ const rows = this.db
96
+ .prepare(`SELECT wi.*, i.symbol, i.name, i.asset_type, i.exchange, i.currency
97
+ FROM watchlist_items wi
98
+ JOIN instruments i ON i.id = wi.instrument_id
99
+ WHERE wi.watchlist_id = ?
100
+ ORDER BY i.symbol`)
101
+ .all(watchlistId);
102
+ return rows.map(mapWatchlistItem);
103
+ }
104
+ removeWatchlistItemBySymbol(symbol, watchlistId = this.getDefaultWatchlist().id) {
105
+ const result = this.db
106
+ .prepare(`DELETE FROM watchlist_items
107
+ WHERE watchlist_id = ?
108
+ AND instrument_id IN (SELECT id FROM instruments WHERE symbol = ?)`)
109
+ .run(watchlistId, symbol.trim().toUpperCase());
110
+ return result.changes > 0;
111
+ }
112
+ updateWatchlistItemBySymbol(symbol, params) {
113
+ const watchlistId = params.watchlistId ?? this.getDefaultWatchlist().id;
114
+ const existing = this.db
115
+ .prepare(`SELECT wi.*
116
+ FROM watchlist_items wi
117
+ JOIN instruments i ON i.id = wi.instrument_id
118
+ WHERE wi.watchlist_id = ? AND i.symbol = ?
119
+ LIMIT 1`)
120
+ .get(watchlistId, symbol.trim().toUpperCase());
121
+ if (existing == null)
122
+ return null;
123
+ const now = new Date().toISOString();
124
+ this.db
125
+ .prepare(`UPDATE watchlist_items
126
+ SET target_price = ?, stop_price = ?, price_currency = ?, thesis = ?,
127
+ notes = ?, tags_json = ?, updated_at = ?
128
+ WHERE id = ?`)
129
+ .run(params.targetPrice === undefined ? existing.target_price : params.targetPrice, params.stopPrice === undefined ? existing.stop_price : params.stopPrice, params.priceCurrency === undefined ? existing.price_currency : params.priceCurrency, params.thesis === undefined ? existing.thesis : params.thesis, params.notes === undefined ? existing.notes : params.notes, params.tags == null ? existing.tags_json : JSON.stringify(params.tags), now, existing.id);
130
+ return this.getWatchlistItem(existing.id);
131
+ }
132
+ addPortfolioLot(params) {
133
+ assertPositiveFinitePortfolioLotNumber(params.quantity, "quantity");
134
+ assertPositiveFinitePortfolioLotNumber(params.avgCost, "average cost");
135
+ const tx = this.db.transaction(() => {
136
+ const portfolioId = params.portfolioId ?? this.getDefaultPortfolio().id;
137
+ const instrument = this.upsertInstrument(params.instrument);
138
+ const now = new Date().toISOString();
139
+ const result = this.db
140
+ .prepare(`INSERT INTO portfolio_lots (
141
+ portfolio_id, instrument_id, quantity, avg_cost, currency,
142
+ opened_at, notes, source, source_account_ref, source_lot_id,
143
+ source_row_id, source_metadata_json, created_at, updated_at
144
+ )
145
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
146
+ .run(portfolioId, instrument.id, params.quantity, params.avgCost, params.currency.toUpperCase(), params.openedAt ?? now, params.notes ?? null, normalizeNullable(params.source), normalizeNullable(params.sourceAccountRef), normalizeNullable(params.sourceLotId), normalizeNullable(params.sourceRowId), params.sourceMetadata == null ? null : JSON.stringify(params.sourceMetadata), now, now);
147
+ return Number(result.lastInsertRowid);
148
+ });
149
+ return this.getPortfolioLot(tx());
150
+ }
151
+ listPortfolioLots(portfolioId = this.getDefaultPortfolio().id) {
152
+ const rows = this.db
153
+ .prepare(`SELECT pl.*, i.symbol, i.name, i.asset_type, i.exchange, i.currency AS instrument_currency
154
+ FROM portfolio_lots pl
155
+ JOIN instruments i ON i.id = pl.instrument_id
156
+ WHERE pl.portfolio_id = ?
157
+ ORDER BY pl.id`)
158
+ .all(portfolioId);
159
+ return rows.map(mapPortfolioLot);
160
+ }
161
+ removePortfolioLotsBySymbol(symbol, portfolioId = this.getDefaultPortfolio().id) {
162
+ const result = this.db
163
+ .prepare(`DELETE FROM portfolio_lots
164
+ WHERE portfolio_id = ?
165
+ AND instrument_id IN (SELECT id FROM instruments WHERE symbol = ?)`)
166
+ .run(portfolioId, symbol.trim().toUpperCase());
167
+ return result.changes > 0;
168
+ }
169
+ removePortfolioLot(id) {
170
+ const existing = this.getPortfolioLotOrNull(id);
171
+ if (existing == null)
172
+ return null;
173
+ this.db.prepare("DELETE FROM portfolio_lots WHERE id = ?").run(id);
174
+ return existing;
175
+ }
176
+ updatePortfolioLot(id, params) {
177
+ if (params.quantity != null) {
178
+ assertPositiveFinitePortfolioLotNumber(params.quantity, "quantity");
179
+ }
180
+ if (params.avgCost != null) {
181
+ assertPositiveFinitePortfolioLotNumber(params.avgCost, "average cost");
182
+ }
183
+ const existing = this.db.prepare("SELECT * FROM portfolio_lots WHERE id = ?").get(id);
184
+ if (existing == null)
185
+ return null;
186
+ const now = new Date().toISOString();
187
+ this.db
188
+ .prepare(`UPDATE portfolio_lots
189
+ SET quantity = ?, avg_cost = ?, currency = ?, opened_at = ?, notes = ?, updated_at = ?
190
+ WHERE id = ?`)
191
+ .run(params.quantity ?? existing.quantity, params.avgCost ?? existing.avg_cost, params.currency == null ? existing.currency : params.currency.toUpperCase(), params.openedAt ?? existing.opened_at, params.notes ?? existing.notes, now, id);
192
+ return this.getPortfolioLot(id);
193
+ }
194
+ updatePortfolioLotsBySymbol(symbol, params) {
195
+ const portfolioId = params.portfolioId ?? this.getDefaultPortfolio().id;
196
+ const rows = this.db
197
+ .prepare(`SELECT pl.*
198
+ FROM portfolio_lots pl
199
+ JOIN instruments i ON i.id = pl.instrument_id
200
+ WHERE pl.portfolio_id = ? AND i.symbol = ?
201
+ ORDER BY pl.id`)
202
+ .all(portfolioId, symbol.trim().toUpperCase());
203
+ return rows.flatMap((row) => {
204
+ const updated = this.updatePortfolioLot(row.id, params);
205
+ return updated == null ? [] : [updated];
206
+ });
207
+ }
208
+ recordPrediction(params) {
209
+ const tx = this.db.transaction(() => {
210
+ const instrument = this.upsertInstrument(params.instrument);
211
+ const opened = params.now ?? new Date();
212
+ const expires = new Date(opened);
213
+ expires.setDate(expires.getDate() + params.timeframeDays);
214
+ const nowIso = opened.toISOString();
215
+ const result = this.db
216
+ .prepare(`INSERT INTO prediction_records (
217
+ instrument_id, direction, conviction, entry_price, target_price,
218
+ opened_at, expires_at, status, created_at, updated_at
219
+ )
220
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, ?)`)
221
+ .run(instrument.id, params.direction, params.conviction, params.entryPrice, params.targetPrice ?? null, nowIso, expires.toISOString(), nowIso, nowIso);
222
+ return Number(result.lastInsertRowid);
223
+ });
224
+ return this.getPrediction(tx());
225
+ }
226
+ listPredictions() {
227
+ const rows = this.db
228
+ .prepare(`SELECT pr.*, i.symbol
229
+ FROM prediction_records pr
230
+ JOIN instruments i ON i.id = pr.instrument_id
231
+ ORDER BY pr.opened_at, pr.id`)
232
+ .all();
233
+ return rows.map(mapPrediction);
234
+ }
235
+ updatePredictionOutcome(params) {
236
+ const now = new Date().toISOString();
237
+ this.db
238
+ .prepare(`UPDATE prediction_records
239
+ SET status = ?, resolved_at = ?, result_json = ?, updated_at = ?
240
+ WHERE id = ?`)
241
+ .run(params.status, params.resolvedAt, JSON.stringify(params.result), now, params.id);
242
+ return this.getPrediction(params.id);
243
+ }
244
+ createAlertRule(params) {
245
+ const now = new Date().toISOString();
246
+ const result = this.db
247
+ .prepare(`INSERT INTO alert_rules (
248
+ scope_type, scope_id, instrument_id, condition_type, condition_version,
249
+ condition_json, timeframe, enabled, check_interval_seconds, next_check_at,
250
+ retrigger_mode, cooldown_seconds, created_at, updated_at
251
+ )
252
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
253
+ .run(params.scopeType, params.scopeId ?? null, params.instrumentId ?? null, params.conditionType, params.conditionVersion, JSON.stringify(params.condition), params.timeframe, params.enabled === false ? 0 : 1, params.checkIntervalSeconds ?? null, params.nextCheckAt ?? null, params.retriggerMode ?? "recurring", params.cooldownSeconds ?? null, now, now);
254
+ return this.getAlertRule(Number(result.lastInsertRowid));
255
+ }
256
+ listAlertRules() {
257
+ const rows = this.db
258
+ .prepare("SELECT * FROM alert_rules ORDER BY created_at, id")
259
+ .all();
260
+ return rows.map(mapAlertRule);
261
+ }
262
+ setAlertRuleEnabled(id, enabled) {
263
+ const now = new Date().toISOString();
264
+ const result = this.db
265
+ .prepare(`UPDATE alert_rules
266
+ SET enabled = ?, updated_at = ?
267
+ WHERE id = ?`)
268
+ .run(enabled ? 1 : 0, now, id);
269
+ if (result.changes === 0)
270
+ return null;
271
+ return this.getAlertRule(id);
272
+ }
273
+ getInstrument(id) {
274
+ const row = this.db.prepare("SELECT * FROM instruments WHERE id = ?").get(id);
275
+ return row == null ? null : mapInstrument(row);
276
+ }
277
+ upsertInstrumentRecord(input) {
278
+ return mapInstrument(this.upsertInstrument(input));
279
+ }
280
+ acquireAutomationRunnerLease(params) {
281
+ const now = params.now ?? new Date().toISOString();
282
+ const nowMs = new Date(now).getTime();
283
+ const expiresAt = new Date(nowMs + params.ttlSeconds * 1000).toISOString();
284
+ const tx = this.db.transaction(() => {
285
+ const current = this.db
286
+ .prepare("SELECT owner_id, owner_kind, acquired_at, heartbeat_at, expires_at FROM automation_runner_leases WHERE id = 1")
287
+ .get();
288
+ if (current != null &&
289
+ current.owner_id !== params.ownerId &&
290
+ new Date(current.expires_at).getTime() > nowMs) {
291
+ return { acquired: false, ...mapAutomationRunnerLease(current) };
292
+ }
293
+ this.db
294
+ .prepare(`INSERT INTO automation_runner_leases (
295
+ id, owner_id, owner_kind, acquired_at, heartbeat_at, expires_at
296
+ )
297
+ VALUES (1, ?, ?, ?, ?, ?)
298
+ ON CONFLICT(id) DO UPDATE SET
299
+ owner_id = excluded.owner_id,
300
+ owner_kind = excluded.owner_kind,
301
+ acquired_at = excluded.acquired_at,
302
+ heartbeat_at = excluded.heartbeat_at,
303
+ expires_at = excluded.expires_at`)
304
+ .run(params.ownerId, params.ownerKind, now, now, expiresAt);
305
+ return {
306
+ acquired: true,
307
+ ownerId: params.ownerId,
308
+ ownerKind: params.ownerKind,
309
+ acquiredAt: now,
310
+ heartbeatAt: now,
311
+ expiresAt,
312
+ };
313
+ });
314
+ return tx();
315
+ }
316
+ getAutomationRunnerLease(now = new Date().toISOString()) {
317
+ const row = this.db
318
+ .prepare("SELECT owner_id, owner_kind, acquired_at, heartbeat_at, expires_at FROM automation_runner_leases WHERE id = 1")
319
+ .get();
320
+ if (row == null)
321
+ return null;
322
+ if (new Date(row.expires_at).getTime() <= new Date(now).getTime())
323
+ return null;
324
+ return mapAutomationRunnerLease(row);
325
+ }
326
+ releaseAutomationRunnerLease(ownerId) {
327
+ const result = this.db
328
+ .prepare("DELETE FROM automation_runner_leases WHERE id = 1 AND owner_id = ?")
329
+ .run(ownerId);
330
+ return result.changes > 0;
331
+ }
332
+ startAlertCheckRun(params) {
333
+ const startedAt = params.startedAt ?? new Date().toISOString();
334
+ const result = this.db
335
+ .prepare(`INSERT INTO alert_check_runs (
336
+ started_at, status, trigger_type, owner_id
337
+ )
338
+ VALUES (?, 'running', ?, ?)`)
339
+ .run(startedAt, params.triggerType, params.ownerId ?? null);
340
+ return this.getAlertCheckRun(Number(result.lastInsertRowid));
341
+ }
342
+ completeAlertCheckRun(id, params) {
343
+ const completedAt = params.completedAt ?? new Date().toISOString();
344
+ this.db
345
+ .prepare(`UPDATE alert_check_runs
346
+ SET completed_at = ?,
347
+ status = ?,
348
+ checked_count = ?,
349
+ triggered_count = ?,
350
+ unavailable_count = ?,
351
+ error_json = ?,
352
+ provider_status_json = ?
353
+ WHERE id = ?`)
354
+ .run(completedAt, params.status, params.checkedCount, params.triggeredCount, params.unavailableCount, params.error == null ? null : JSON.stringify(params.error), params.providerStatus == null ? null : JSON.stringify(params.providerStatus), id);
355
+ return this.getAlertCheckRun(id);
356
+ }
357
+ getAlertCheckRun(id) {
358
+ const row = this.db.prepare("SELECT * FROM alert_check_runs WHERE id = ?").get(id);
359
+ if (row == null)
360
+ throw new Error(`alert check run ${id} not found`);
361
+ return mapAlertCheckRun(row);
362
+ }
363
+ listAlertCheckRuns() {
364
+ const rows = this.db
365
+ .prepare("SELECT * FROM alert_check_runs ORDER BY started_at DESC, id DESC")
366
+ .all();
367
+ return rows.map(mapAlertCheckRun);
368
+ }
369
+ markStaleAutomationRunsLost(params) {
370
+ const now = params.now ?? new Date().toISOString();
371
+ const cutoff = new Date(new Date(now).getTime() - params.graceSeconds * 1000).toISOString();
372
+ const tx = this.db.transaction(() => {
373
+ const alertResult = this.db
374
+ .prepare(`UPDATE alert_check_runs
375
+ SET status = 'lost',
376
+ completed_at = COALESCE(completed_at, ?),
377
+ error_json = COALESCE(error_json, ?)
378
+ WHERE status = 'running' AND started_at < ?`)
379
+ .run(now, JSON.stringify({ message: "runner lease expired before completion" }), cutoff);
380
+ const reportResult = this.db
381
+ .prepare(`UPDATE report_runs
382
+ SET status = 'lost',
383
+ completed_at = COALESCE(completed_at, ?),
384
+ errors_json = COALESCE(errors_json, ?)
385
+ WHERE status = 'running' AND started_at < ?`)
386
+ .run(now, JSON.stringify(["runner lease expired before completion"]), cutoff);
387
+ return {
388
+ alertCheckRuns: alertResult.changes,
389
+ reportRuns: reportResult.changes,
390
+ };
391
+ });
392
+ return tx();
393
+ }
394
+ recordNotificationEvent(params) {
395
+ const createdAt = params.createdAt ?? new Date().toISOString();
396
+ const result = this.db
397
+ .prepare(`INSERT INTO notification_events (
398
+ source_type, source_id, severity, title, body, payload_json, status, created_at
399
+ )
400
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
401
+ .run(params.sourceType, params.sourceId ?? null, params.severity, params.title, params.body, params.payload == null ? null : JSON.stringify(params.payload), params.status ?? "unread", createdAt);
402
+ return this.getNotificationEvent(Number(result.lastInsertRowid));
403
+ }
404
+ listNotificationEvents() {
405
+ const rows = this.db
406
+ .prepare("SELECT * FROM notification_events ORDER BY created_at DESC, id DESC")
407
+ .all();
408
+ return rows.map(mapNotificationEvent);
409
+ }
410
+ getNotificationEvent(id) {
411
+ const row = this.db.prepare("SELECT * FROM notification_events WHERE id = ?").get(id);
412
+ if (row == null)
413
+ throw new Error(`notification event ${id} not found`);
414
+ return mapNotificationEvent(row);
415
+ }
416
+ acknowledgeNotificationEvent(id, acknowledgedAt = new Date().toISOString()) {
417
+ this.db
418
+ .prepare(`UPDATE notification_events
419
+ SET status = 'acknowledged',
420
+ acknowledged_at = ?
421
+ WHERE id = ?`)
422
+ .run(acknowledgedAt, id);
423
+ return this.getNotificationEvent(id);
424
+ }
425
+ recordNotificationDeliveryAttempt(params) {
426
+ const attemptedAt = params.attemptedAt ?? new Date().toISOString();
427
+ const result = this.db
428
+ .prepare(`INSERT INTO notification_delivery_attempts (
429
+ notification_event_id, channel, status, attempted_at, completed_at,
430
+ response_json, error
431
+ )
432
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
433
+ .run(params.notificationEventId, params.channel, params.status, attemptedAt, params.completedAt ?? null, params.response == null ? null : JSON.stringify(params.response), params.error ?? null);
434
+ return this.getNotificationDeliveryAttempt(Number(result.lastInsertRowid));
435
+ }
436
+ listNotificationDeliveryAttempts(notificationEventId) {
437
+ const rows = notificationEventId == null
438
+ ? this.db
439
+ .prepare("SELECT * FROM notification_delivery_attempts ORDER BY attempted_at DESC, id DESC")
440
+ .all()
441
+ : this.db
442
+ .prepare("SELECT * FROM notification_delivery_attempts WHERE notification_event_id = ? ORDER BY attempted_at DESC, id DESC")
443
+ .all(notificationEventId);
444
+ return rows.map(mapNotificationDeliveryAttempt);
445
+ }
446
+ getNotificationDeliveryAttempt(id) {
447
+ const row = this.db
448
+ .prepare("SELECT * FROM notification_delivery_attempts WHERE id = ?")
449
+ .get(id);
450
+ if (row == null)
451
+ throw new Error(`notification delivery attempt ${id} not found`);
452
+ return mapNotificationDeliveryAttempt(row);
453
+ }
454
+ updateAlertObservation(params) {
455
+ const checkedAt = params.checkedAt ?? new Date().toISOString();
456
+ this.db
457
+ .prepare(`UPDATE alert_rules
458
+ SET last_checked_at = ?,
459
+ last_observed_json = ?,
460
+ last_triggered_at = COALESCE(?, last_triggered_at),
461
+ updated_at = ?
462
+ WHERE id = ?`)
463
+ .run(checkedAt, JSON.stringify(params.observed), params.triggeredAt ?? null, checkedAt, params.ruleId);
464
+ return this.getAlertRule(params.ruleId);
465
+ }
466
+ recordAlertEvent(params) {
467
+ const triggeredAt = params.triggeredAt ?? new Date().toISOString();
468
+ const result = this.db
469
+ .prepare(`INSERT INTO alert_events (
470
+ alert_rule_id, instrument_id, observed_value_json, triggered_at, status, message
471
+ )
472
+ VALUES (?, ?, ?, ?, ?, ?)`)
473
+ .run(params.alertRuleId, params.instrumentId ?? null, JSON.stringify(params.observedValue), triggeredAt, params.status, params.message);
474
+ return this.getAlertEvent(Number(result.lastInsertRowid));
475
+ }
476
+ recordAlertCheckResult(params) {
477
+ const tx = this.db.transaction(() => {
478
+ const row = this.db.prepare("SELECT * FROM alert_rules WHERE id = ?").get(params.ruleId);
479
+ if (row == null) {
480
+ throw new Error(`alert rule ${params.ruleId} not found`);
481
+ }
482
+ const currentPrevious = lastObservedValueFromJson(row.last_observed_json);
483
+ const currentLastTriggeredAt = row.last_triggered_at ?? null;
484
+ const canTrigger = params.trigger != null &&
485
+ currentPrevious === params.trigger.expectedPreviousValue &&
486
+ currentLastTriggeredAt === params.trigger.expectedLastTriggeredAt;
487
+ if (canTrigger && params.trigger) {
488
+ this.db
489
+ .prepare(`INSERT INTO alert_events (
490
+ alert_rule_id, instrument_id, observed_value_json, triggered_at, status, message
491
+ )
492
+ VALUES (?, ?, ?, ?, 'triggered', ?)`)
493
+ .run(params.ruleId, params.trigger.instrumentId, JSON.stringify(params.observed), params.trigger.triggeredAt, params.trigger.message);
494
+ }
495
+ this.db
496
+ .prepare(`UPDATE alert_rules
497
+ SET last_checked_at = ?,
498
+ last_observed_json = ?,
499
+ last_triggered_at = COALESCE(?, last_triggered_at),
500
+ updated_at = ?
501
+ WHERE id = ?`)
502
+ .run(params.checkedAt, JSON.stringify(params.observed), canTrigger && params.trigger ? params.trigger.triggeredAt : null, params.checkedAt, params.ruleId);
503
+ return canTrigger;
504
+ });
505
+ const triggered = tx();
506
+ return { triggered, rule: this.getAlertRule(params.ruleId) };
507
+ }
508
+ recordAlertEvaluationResult(params) {
509
+ const tx = this.db.transaction(() => {
510
+ const row = this.db.prepare("SELECT * FROM alert_rules WHERE id = ?").get(params.ruleId);
511
+ if (row == null) {
512
+ throw new Error(`alert rule ${params.ruleId} not found`);
513
+ }
514
+ const rearmed = params.conditionState === "false" && row.last_condition_state === "true";
515
+ const nextArmCycleId = rearmed ? row.arm_cycle_id + 1 : row.arm_cycle_id;
516
+ let eventId = null;
517
+ const canInsertTrigger = params.trigger != null &&
518
+ row.enabled === 1 &&
519
+ row.status === "active" &&
520
+ row.last_condition_state !== "true";
521
+ if (params.trigger != null && canInsertTrigger) {
522
+ const result = this.db
523
+ .prepare(`INSERT OR IGNORE INTO alert_events (
524
+ alert_rule_id, instrument_id, observed_value_json, triggered_at,
525
+ observed_at, provider_data_at, source_provider, cache_status,
526
+ data_delay_ms, trigger_source, dedupe_key, status, message
527
+ )
528
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
529
+ .run(params.ruleId, params.trigger.instrumentId, JSON.stringify(params.observed), params.trigger.triggeredAt, params.trigger.observedAt, params.trigger.providerDataAt ?? null, params.trigger.sourceProvider ?? null, params.trigger.cacheStatus ?? "live", params.trigger.dataDelayMs ?? null, params.trigger.triggerSource, params.trigger.dedupeKey, params.trigger.status ?? "triggered", params.trigger.message);
530
+ if (result.changes > 0) {
531
+ eventId = Number(result.lastInsertRowid);
532
+ this.db
533
+ .prepare(`INSERT INTO notification_events (
534
+ source_type, source_id, severity, title, body, payload_json, status, created_at
535
+ )
536
+ VALUES ('alert_event', ?, 'warning', ?, ?, ?, 'unread', ?)`)
537
+ .run(eventId, params.trigger.title ?? "Alert triggered", params.trigger.message, JSON.stringify({
538
+ ruleId: params.ruleId,
539
+ observed: params.observed,
540
+ sourceProvider: params.trigger.sourceProvider ?? null,
541
+ providerDataAt: params.trigger.providerDataAt ?? null,
542
+ cacheStatus: params.trigger.cacheStatus ?? "live",
543
+ dataDelayMs: params.trigger.dataDelayMs ?? null,
544
+ }), params.trigger.observedAt);
545
+ }
546
+ }
547
+ const triggered = eventId != null;
548
+ const completeOneShot = triggered && row.retrigger_mode === "once";
549
+ this.db
550
+ .prepare(`UPDATE alert_rules
551
+ SET last_checked_at = ?,
552
+ last_observed_json = ?,
553
+ last_condition_state = ?,
554
+ arm_cycle_id = ?,
555
+ next_check_at = ?,
556
+ last_triggered_at = COALESCE(?, last_triggered_at),
557
+ enabled = CASE WHEN ? THEN 0 ELSE enabled END,
558
+ status = CASE WHEN ? THEN 'completed' ELSE status END,
559
+ updated_at = ?
560
+ WHERE id = ?`)
561
+ .run(params.checkedAt, JSON.stringify(params.observed), params.conditionState, nextArmCycleId, nextAlertCheckAt(row, params.checkedAt), triggered && params.trigger ? params.trigger.triggeredAt : null, completeOneShot ? 1 : 0, completeOneShot ? 1 : 0, params.checkedAt, params.ruleId);
562
+ return { triggered, eventId };
563
+ });
564
+ const result = tx();
565
+ return {
566
+ triggered: result.triggered,
567
+ event: result.eventId == null ? null : this.getAlertEvent(result.eventId),
568
+ rule: this.getAlertRule(params.ruleId),
569
+ };
570
+ }
571
+ recordAlertUnavailable(params) {
572
+ const tx = this.db.transaction(() => {
573
+ const row = this.db.prepare("SELECT * FROM alert_rules WHERE id = ?").get(params.ruleId);
574
+ if (row == null) {
575
+ throw new Error(`alert rule ${params.ruleId} not found`);
576
+ }
577
+ const result = this.db
578
+ .prepare(`INSERT INTO alert_events (
579
+ alert_rule_id, instrument_id, observed_value_json, triggered_at, status, message
580
+ )
581
+ VALUES (?, ?, ?, ?, 'unavailable', ?)`)
582
+ .run(params.ruleId, params.instrumentId ?? null, JSON.stringify({ status: "unavailable", reason: params.reason, at: params.checkedAt }), params.checkedAt, `Alert unavailable: ${params.reason}`);
583
+ this.db
584
+ .prepare(`UPDATE alert_rules
585
+ SET last_checked_at = ?,
586
+ next_check_at = ?,
587
+ updated_at = ?
588
+ WHERE id = ?`)
589
+ .run(params.checkedAt, nextAlertCheckAt(row, params.checkedAt), params.checkedAt, params.ruleId);
590
+ return Number(result.lastInsertRowid);
591
+ });
592
+ const eventId = tx();
593
+ return { event: this.getAlertEvent(eventId), rule: this.getAlertRule(params.ruleId) };
594
+ }
595
+ listAlertEvents() {
596
+ const rows = this.db
597
+ .prepare("SELECT * FROM alert_events ORDER BY triggered_at, id")
598
+ .all();
599
+ return rows.map(mapAlertEvent);
600
+ }
601
+ createReportTemplate(params) {
602
+ const now = new Date().toISOString();
603
+ const result = this.db
604
+ .prepare(`INSERT INTO report_templates (
605
+ name, report_type, cadence, timezone, local_time, config_json,
606
+ enabled, next_run_at, created_at, updated_at
607
+ )
608
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
609
+ .run(params.name, params.reportType, params.cadence, params.timezone, params.localTime, JSON.stringify(params.config), params.enabled === false ? 0 : 1, params.nextRunAt ?? null, now, now);
610
+ return this.getReportTemplate(Number(result.lastInsertRowid));
611
+ }
612
+ updateReportTemplate(id, params) {
613
+ const existing = this.getReportTemplate(id);
614
+ const now = new Date().toISOString();
615
+ this.db
616
+ .prepare(`UPDATE report_templates
617
+ SET name = ?, report_type = ?, cadence = ?, timezone = ?, local_time = ?,
618
+ config_json = ?, enabled = ?, next_run_at = ?, updated_at = ?
619
+ WHERE id = ?`)
620
+ .run(params.name ?? existing.name, params.reportType ?? existing.reportType, params.cadence ?? existing.cadence, params.timezone ?? existing.timezone, params.localTime ?? existing.localTime, JSON.stringify(params.config ?? existing.configJson), params.enabled == null ? (existing.enabled ? 1 : 0) : params.enabled ? 1 : 0, params.nextRunAt === undefined ? existing.nextRunAt : params.nextRunAt, now, id);
621
+ return this.getReportTemplate(id);
622
+ }
623
+ claimDueReportTemplateRun(id, params) {
624
+ const result = this.db
625
+ .prepare(`UPDATE report_templates
626
+ SET next_run_at = ?, updated_at = ?
627
+ WHERE id = ? AND enabled = 1 AND next_run_at = ?`)
628
+ .run(params.nextRunAt, params.claimedAt ?? new Date().toISOString(), id, params.scheduledFor);
629
+ return result.changes === 0 ? null : this.getReportTemplate(id);
630
+ }
631
+ listReportTemplates() {
632
+ const rows = this.db
633
+ .prepare("SELECT * FROM report_templates ORDER BY created_at, id")
634
+ .all();
635
+ return rows.map(mapReportTemplate);
636
+ }
637
+ recordReportRun(params) {
638
+ const startedAt = params.startedAt ?? new Date().toISOString();
639
+ const tx = this.db.transaction(() => {
640
+ const result = this.db
641
+ .prepare(`INSERT INTO report_runs (
642
+ template_id, started_at, completed_at, status, trigger_type, scheduled_for,
643
+ owner_id, artifact_path, summary_json, errors_json
644
+ )
645
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
646
+ .run(params.templateId ?? null, startedAt, params.completedAt ?? null, params.status, params.triggerType ?? "manual", params.scheduledFor ?? null, params.ownerId ?? null, params.artifactPath ?? null, params.summary == null ? null : JSON.stringify(params.summary), params.errors == null ? null : JSON.stringify(params.errors));
647
+ if (params.templateId != null) {
648
+ this.db
649
+ .prepare("UPDATE report_templates SET last_run_at = ?, updated_at = ? WHERE id = ?")
650
+ .run(startedAt, startedAt, params.templateId);
651
+ }
652
+ return Number(result.lastInsertRowid);
653
+ });
654
+ return this.getReportRun(tx());
655
+ }
656
+ listReportRuns() {
657
+ const rows = this.db
658
+ .prepare("SELECT * FROM report_runs ORDER BY started_at DESC, id DESC")
659
+ .all();
660
+ return rows.map(mapReportRun);
661
+ }
662
+ recordImportBatch(params) {
663
+ const importedAt = params.importedAt ?? new Date().toISOString();
664
+ const result = this.db
665
+ .prepare(`INSERT INTO import_batches (
666
+ source, source_label, imported_at, status, raw_metadata_json
667
+ )
668
+ VALUES (?, ?, ?, ?, ?)`)
669
+ .run(normalizeNullable(params.source) ?? "unknown", normalizeNullable(params.sourceLabel), importedAt, params.status, params.rawMetadata == null ? null : JSON.stringify(params.rawMetadata));
670
+ return this.getImportBatch(Number(result.lastInsertRowid));
671
+ }
672
+ recordImportRow(params) {
673
+ const result = this.db
674
+ .prepare(`INSERT INTO import_rows (
675
+ batch_id, row_type, source_symbol, source_row_id, source_account_ref,
676
+ normalized_instrument_id, status, error, source_metadata_json, raw_json
677
+ )
678
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
679
+ .run(params.batchId, params.rowType, normalizeNullable(params.sourceSymbol), normalizeNullable(params.sourceRowId), normalizeNullable(params.sourceAccountRef), params.normalizedInstrumentId ?? null, params.status, normalizeNullable(params.error), params.sourceMetadata == null ? null : JSON.stringify(params.sourceMetadata), params.raw == null ? null : JSON.stringify(params.raw));
680
+ return this.getImportRow(Number(result.lastInsertRowid));
681
+ }
682
+ upsertInstrument(input) {
683
+ const symbol = input.symbol.trim().toUpperCase();
684
+ const assetType = input.assetType.trim().toLowerCase();
685
+ const provider = input.provider.trim().toLowerCase();
686
+ const exchange = normalizeNullable(input.exchange);
687
+ const now = new Date().toISOString();
688
+ const resolvedAt = (input.resolvedAt ?? new Date()).toISOString();
689
+ const existing = this.db
690
+ .prepare(`SELECT * FROM instruments
691
+ WHERE provider = ?
692
+ AND symbol = ?
693
+ AND asset_type = ?
694
+ AND IFNULL(exchange, '') = IFNULL(?, '')`)
695
+ .get(provider, symbol, assetType, exchange);
696
+ if (existing) {
697
+ this.db
698
+ .prepare(`UPDATE instruments
699
+ SET name = ?, currency = ?, provider_metadata_json = ?,
700
+ last_resolved_at = ?, updated_at = ?
701
+ WHERE id = ?`)
702
+ .run(normalizeNullable(input.name), normalizeNullable(input.currency)?.toUpperCase() ?? null, input.providerMetadata == null ? null : JSON.stringify(input.providerMetadata), resolvedAt, now, existing.id);
703
+ this.upsertInstrumentAliases(existing.id, input.aliases ?? []);
704
+ return this.db
705
+ .prepare("SELECT * FROM instruments WHERE id = ?")
706
+ .get(existing.id);
707
+ }
708
+ const result = this.db
709
+ .prepare(`INSERT INTO instruments (
710
+ symbol, asset_type, name, exchange, currency, provider,
711
+ provider_metadata_json, last_resolved_at, created_at, updated_at
712
+ )
713
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
714
+ .run(symbol, assetType, normalizeNullable(input.name), exchange, normalizeNullable(input.currency)?.toUpperCase() ?? null, provider, input.providerMetadata == null ? null : JSON.stringify(input.providerMetadata), resolvedAt, now, now);
715
+ const instrumentId = Number(result.lastInsertRowid);
716
+ this.upsertInstrumentAliases(instrumentId, input.aliases ?? []);
717
+ return this.db
718
+ .prepare("SELECT * FROM instruments WHERE id = ?")
719
+ .get(instrumentId);
720
+ }
721
+ upsertInstrumentAliases(instrumentId, aliases) {
722
+ if (aliases.length === 0)
723
+ return;
724
+ const now = new Date().toISOString();
725
+ for (const alias of aliases) {
726
+ const source = normalizeSource(alias.source);
727
+ const sourceSymbol = normalizeSourceSymbol(alias.sourceSymbol);
728
+ const sourceExchange = normalizeExchange(alias.sourceExchange);
729
+ const sourceAssetType = normalizeAssetType(alias.sourceAssetType);
730
+ const sourceId = normalizeNullable(alias.sourceId);
731
+ const rawJson = alias.raw == null ? null : JSON.stringify(alias.raw);
732
+ const existing = sourceId == null
733
+ ? this.db
734
+ .prepare(`SELECT id, instrument_id FROM instrument_aliases
735
+ WHERE source = ?
736
+ AND source_symbol = ?
737
+ AND IFNULL(source_exchange, '') = IFNULL(?, '')
738
+ AND IFNULL(source_asset_type, '') = IFNULL(?, '')
739
+ LIMIT 1`)
740
+ .get(source, sourceSymbol, sourceExchange, sourceAssetType)
741
+ : this.db
742
+ .prepare(`SELECT id, instrument_id FROM instrument_aliases
743
+ WHERE source = ? AND source_id = ?
744
+ LIMIT 1`)
745
+ .get(source, sourceId);
746
+ if (existing) {
747
+ this.db
748
+ .prepare(`UPDATE instrument_aliases
749
+ SET instrument_id = ?, source_symbol = ?, source_exchange = ?,
750
+ source_asset_type = ?, source_id = ?, raw_json = ?, updated_at = ?
751
+ WHERE id = ?`)
752
+ .run(instrumentId, sourceSymbol, sourceExchange, sourceAssetType, sourceId, rawJson, now, existing.id);
753
+ continue;
754
+ }
755
+ this.db
756
+ .prepare(`INSERT INTO instrument_aliases (
757
+ instrument_id, source, source_symbol, source_exchange,
758
+ source_asset_type, source_id, raw_json, created_at, updated_at
759
+ )
760
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
761
+ .run(instrumentId, source, sourceSymbol, sourceExchange, sourceAssetType, sourceId, rawJson, now, now);
762
+ }
763
+ }
764
+ getWatchlistItem(id) {
765
+ const row = this.db
766
+ .prepare(`SELECT wi.*, i.symbol, i.name, i.asset_type, i.exchange, i.currency
767
+ FROM watchlist_items wi
768
+ JOIN instruments i ON i.id = wi.instrument_id
769
+ WHERE wi.id = ?`)
770
+ .get(id);
771
+ return mapWatchlistItem(row);
772
+ }
773
+ getPortfolioLot(id) {
774
+ const row = this.getPortfolioLotOrNull(id);
775
+ if (row == null) {
776
+ throw new Error(`portfolio lot ${id} not found`);
777
+ }
778
+ return row;
779
+ }
780
+ getPortfolioLotOrNull(id) {
781
+ const row = this.db
782
+ .prepare(`SELECT pl.*, i.symbol, i.name, i.asset_type, i.exchange, i.currency AS instrument_currency
783
+ FROM portfolio_lots pl
784
+ JOIN instruments i ON i.id = pl.instrument_id
785
+ WHERE pl.id = ?`)
786
+ .get(id);
787
+ return row == null ? null : mapPortfolioLot(row);
788
+ }
789
+ getPrediction(id) {
790
+ const row = this.db
791
+ .prepare(`SELECT pr.*, i.symbol
792
+ FROM prediction_records pr
793
+ JOIN instruments i ON i.id = pr.instrument_id
794
+ WHERE pr.id = ?`)
795
+ .get(id);
796
+ return mapPrediction(row);
797
+ }
798
+ getAlertRule(id) {
799
+ const row = this.db.prepare("SELECT * FROM alert_rules WHERE id = ?").get(id);
800
+ return mapAlertRule(row);
801
+ }
802
+ getAlertEvent(id) {
803
+ const row = this.db.prepare("SELECT * FROM alert_events WHERE id = ?").get(id);
804
+ return mapAlertEvent(row);
805
+ }
806
+ getReportTemplate(id) {
807
+ const row = this.db
808
+ .prepare("SELECT * FROM report_templates WHERE id = ?")
809
+ .get(id);
810
+ return mapReportTemplate(row);
811
+ }
812
+ getReportRun(id) {
813
+ const row = this.db.prepare("SELECT * FROM report_runs WHERE id = ?").get(id);
814
+ return mapReportRun(row);
815
+ }
816
+ getImportBatch(id) {
817
+ const row = this.db
818
+ .prepare("SELECT * FROM import_batches WHERE id = ?")
819
+ .get(id);
820
+ return mapImportBatch(row);
821
+ }
822
+ getImportRow(id) {
823
+ const row = this.db.prepare("SELECT * FROM import_rows WHERE id = ?").get(id);
824
+ return mapImportRow(row);
825
+ }
826
+ }
827
+ function mapCollection(row) {
828
+ return {
829
+ id: row.id,
830
+ name: row.name,
831
+ isDefault: row.is_default === 1,
832
+ createdAt: row.created_at,
833
+ updatedAt: row.updated_at,
834
+ };
835
+ }
836
+ function mapInstrument(row) {
837
+ return {
838
+ id: row.id,
839
+ symbol: row.symbol,
840
+ assetType: row.asset_type,
841
+ name: row.name,
842
+ exchange: row.exchange,
843
+ currency: row.currency,
844
+ provider: row.provider,
845
+ createdAt: row.created_at,
846
+ updatedAt: row.updated_at,
847
+ };
848
+ }
849
+ function mapWatchlistItem(row) {
850
+ return {
851
+ id: row.id,
852
+ watchlistId: row.watchlist_id,
853
+ instrumentId: row.instrument_id,
854
+ symbol: row.symbol,
855
+ name: row.name,
856
+ assetType: row.asset_type,
857
+ exchange: row.exchange,
858
+ currency: row.currency,
859
+ targetPrice: row.target_price,
860
+ stopPrice: row.stop_price,
861
+ priceCurrency: row.price_currency,
862
+ thesis: row.thesis,
863
+ notes: row.notes,
864
+ tags: row.tags_json == null ? null : JSON.parse(row.tags_json),
865
+ source: row.source,
866
+ sourceRowId: row.source_row_id,
867
+ sourceMetadata: row.source_metadata_json == null ? null : JSON.parse(row.source_metadata_json),
868
+ createdAt: row.created_at,
869
+ updatedAt: row.updated_at,
870
+ };
871
+ }
872
+ function mapPortfolioLot(row) {
873
+ return {
874
+ id: row.id,
875
+ portfolioId: row.portfolio_id,
876
+ instrumentId: row.instrument_id,
877
+ symbol: row.symbol,
878
+ name: row.name,
879
+ assetType: row.asset_type,
880
+ exchange: row.exchange,
881
+ instrumentCurrency: row.instrument_currency,
882
+ quantity: row.quantity,
883
+ avgCost: row.avg_cost,
884
+ currency: row.currency,
885
+ openedAt: row.opened_at,
886
+ notes: row.notes,
887
+ source: row.source,
888
+ sourceAccountRef: row.source_account_ref,
889
+ sourceLotId: row.source_lot_id,
890
+ sourceRowId: row.source_row_id,
891
+ sourceMetadata: row.source_metadata_json == null ? null : JSON.parse(row.source_metadata_json),
892
+ createdAt: row.created_at,
893
+ updatedAt: row.updated_at,
894
+ };
895
+ }
896
+ function mapPrediction(row) {
897
+ return {
898
+ id: row.id,
899
+ instrumentId: row.instrument_id,
900
+ symbol: row.symbol,
901
+ direction: row.direction,
902
+ conviction: row.conviction,
903
+ entryPrice: row.entry_price,
904
+ targetPrice: row.target_price,
905
+ openedAt: row.opened_at,
906
+ expiresAt: row.expires_at,
907
+ status: row.status,
908
+ resolvedAt: row.resolved_at,
909
+ resultJson: row.result_json,
910
+ createdAt: row.created_at,
911
+ updatedAt: row.updated_at,
912
+ };
913
+ }
914
+ function mapAlertRule(row) {
915
+ return {
916
+ id: row.id,
917
+ scopeType: row.scope_type,
918
+ scopeId: row.scope_id,
919
+ instrumentId: row.instrument_id,
920
+ conditionType: row.condition_type,
921
+ conditionVersion: row.condition_version,
922
+ conditionJson: JSON.parse(row.condition_json),
923
+ timeframe: row.timeframe,
924
+ enabled: row.enabled === 1,
925
+ checkIntervalSeconds: row.check_interval_seconds,
926
+ nextCheckAt: row.next_check_at,
927
+ lastCheckedAt: row.last_checked_at,
928
+ lastObservedJson: row.last_observed_json == null ? null : JSON.parse(row.last_observed_json),
929
+ status: row.status,
930
+ retriggerMode: row.retrigger_mode,
931
+ lastConditionState: row.last_condition_state,
932
+ ruleRevision: row.rule_revision,
933
+ armCycleId: row.arm_cycle_id,
934
+ cooldownSeconds: row.cooldown_seconds,
935
+ lastTriggeredAt: row.last_triggered_at,
936
+ createdAt: row.created_at,
937
+ updatedAt: row.updated_at,
938
+ };
939
+ }
940
+ function mapAlertEvent(row) {
941
+ return {
942
+ id: row.id,
943
+ alertRuleId: row.alert_rule_id,
944
+ instrumentId: row.instrument_id,
945
+ observedValueJson: row.observed_value_json == null ? null : JSON.parse(row.observed_value_json),
946
+ triggeredAt: row.triggered_at,
947
+ observedAt: row.observed_at,
948
+ providerDataAt: row.provider_data_at,
949
+ sourceProvider: row.source_provider,
950
+ cacheStatus: row.cache_status,
951
+ dataDelayMs: row.data_delay_ms,
952
+ triggerSource: row.trigger_source,
953
+ dedupeKey: row.dedupe_key,
954
+ status: row.status,
955
+ message: row.message,
956
+ };
957
+ }
958
+ function mapAutomationRunnerLease(row) {
959
+ return {
960
+ ownerId: row.owner_id,
961
+ ownerKind: row.owner_kind,
962
+ acquiredAt: row.acquired_at,
963
+ heartbeatAt: row.heartbeat_at,
964
+ expiresAt: row.expires_at,
965
+ };
966
+ }
967
+ function mapAlertCheckRun(row) {
968
+ return {
969
+ id: row.id,
970
+ startedAt: row.started_at,
971
+ completedAt: row.completed_at,
972
+ status: row.status,
973
+ triggerType: row.trigger_type,
974
+ checkedCount: row.checked_count,
975
+ triggeredCount: row.triggered_count,
976
+ unavailableCount: row.unavailable_count,
977
+ ownerId: row.owner_id,
978
+ errorJson: row.error_json == null ? null : JSON.parse(row.error_json),
979
+ providerStatusJson: row.provider_status_json == null ? null : JSON.parse(row.provider_status_json),
980
+ };
981
+ }
982
+ function mapNotificationEvent(row) {
983
+ return {
984
+ id: row.id,
985
+ sourceType: row.source_type,
986
+ sourceId: row.source_id,
987
+ severity: row.severity,
988
+ title: row.title,
989
+ body: row.body,
990
+ payloadJson: row.payload_json == null ? null : JSON.parse(row.payload_json),
991
+ status: row.status,
992
+ createdAt: row.created_at,
993
+ acknowledgedAt: row.acknowledged_at,
994
+ };
995
+ }
996
+ function mapNotificationDeliveryAttempt(row) {
997
+ return {
998
+ id: row.id,
999
+ notificationEventId: row.notification_event_id,
1000
+ channel: row.channel,
1001
+ status: row.status,
1002
+ attemptedAt: row.attempted_at,
1003
+ completedAt: row.completed_at,
1004
+ responseJson: row.response_json == null ? null : JSON.parse(row.response_json),
1005
+ error: row.error,
1006
+ };
1007
+ }
1008
+ function nextAlertCheckAt(row, checkedAt) {
1009
+ if (row.check_interval_seconds == null)
1010
+ return null;
1011
+ const checkedAtMs = new Date(checkedAt).getTime();
1012
+ if (!Number.isFinite(checkedAtMs))
1013
+ return null;
1014
+ return new Date(checkedAtMs + row.check_interval_seconds * 1000).toISOString();
1015
+ }
1016
+ function mapReportTemplate(row) {
1017
+ return {
1018
+ id: row.id,
1019
+ name: row.name,
1020
+ reportType: row.report_type,
1021
+ cadence: row.cadence,
1022
+ timezone: row.timezone,
1023
+ localTime: row.local_time,
1024
+ configJson: JSON.parse(row.config_json),
1025
+ enabled: row.enabled === 1,
1026
+ lastRunAt: row.last_run_at,
1027
+ nextRunAt: row.next_run_at,
1028
+ createdAt: row.created_at,
1029
+ updatedAt: row.updated_at,
1030
+ };
1031
+ }
1032
+ function mapReportRun(row) {
1033
+ return {
1034
+ id: row.id,
1035
+ templateId: row.template_id,
1036
+ startedAt: row.started_at,
1037
+ completedAt: row.completed_at,
1038
+ status: row.status,
1039
+ triggerType: row.trigger_type,
1040
+ scheduledFor: row.scheduled_for,
1041
+ ownerId: row.owner_id,
1042
+ artifactPath: row.artifact_path,
1043
+ summaryJson: row.summary_json == null ? null : JSON.parse(row.summary_json),
1044
+ errorsJson: row.errors_json == null ? null : JSON.parse(row.errors_json),
1045
+ };
1046
+ }
1047
+ function mapImportBatch(row) {
1048
+ return {
1049
+ id: row.id,
1050
+ source: row.source,
1051
+ sourceLabel: row.source_label,
1052
+ importedAt: row.imported_at,
1053
+ status: row.status,
1054
+ rawMetadata: row.raw_metadata_json == null ? null : JSON.parse(row.raw_metadata_json),
1055
+ };
1056
+ }
1057
+ function mapImportRow(row) {
1058
+ return {
1059
+ id: row.id,
1060
+ batchId: row.batch_id,
1061
+ rowType: row.row_type,
1062
+ sourceSymbol: row.source_symbol,
1063
+ sourceRowId: row.source_row_id,
1064
+ sourceAccountRef: row.source_account_ref,
1065
+ normalizedInstrumentId: row.normalized_instrument_id,
1066
+ status: row.status,
1067
+ error: row.error,
1068
+ sourceMetadata: row.source_metadata_json == null ? null : JSON.parse(row.source_metadata_json),
1069
+ raw: row.raw_json == null ? null : JSON.parse(row.raw_json),
1070
+ };
1071
+ }
1072
+ function normalizeNullable(value) {
1073
+ const normalized = value?.trim();
1074
+ return normalized ? normalized : null;
1075
+ }
1076
+ function assertPositiveFinitePortfolioLotNumber(value, label) {
1077
+ if (!Number.isFinite(value) || value <= 0) {
1078
+ throw new Error(`Portfolio lot ${label} must be a positive finite number.`);
1079
+ }
1080
+ }
1081
+ function lastObservedValueFromJson(value) {
1082
+ if (value == null)
1083
+ return null;
1084
+ const parsed = JSON.parse(value);
1085
+ return typeof parsed?.value === "number" ? parsed.value : null;
1086
+ }
1087
+ function normalizeSource(value) {
1088
+ return value.trim().toLowerCase();
1089
+ }
1090
+ function normalizeSourceSymbol(value) {
1091
+ return value.trim().toUpperCase();
1092
+ }
1093
+ function normalizeExchange(value) {
1094
+ return normalizeNullable(value)?.toUpperCase() ?? null;
1095
+ }
1096
+ function normalizeAssetType(value) {
1097
+ return normalizeNullable(value)?.toLowerCase() ?? null;
1098
+ }
1099
+ //# sourceMappingURL=service.js.map