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,399 @@
1
+ import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
2
+ import { httpPost } from "../infra/http-client.js";
3
+ import { rateLimiter } from "../infra/rate-limiter.js";
4
+
5
+ const BASE_URL = "https://scanner.tradingview.com";
6
+ const DATA_CAVEAT =
7
+ "TradingView scanner data may be delayed about 15 minutes and comes from an unofficial endpoint.";
8
+ const BROWSER_UA =
9
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
10
+
11
+ const QUOTE_COLUMNS = [
12
+ "name",
13
+ "close",
14
+ "change",
15
+ "change_abs",
16
+ "volume",
17
+ "exchange",
18
+ "market",
19
+ "description",
20
+ "type",
21
+ "typespecs",
22
+ ] as const;
23
+
24
+ export const DEFAULT_COLUMNS = [
25
+ "name",
26
+ "close",
27
+ "change",
28
+ "volume",
29
+ "market_cap_basic",
30
+ "price_earnings_ttm",
31
+ ] as const;
32
+
33
+ export type ScreenFilterOp =
34
+ | "greater"
35
+ | "egreater"
36
+ | "less"
37
+ | "eless"
38
+ | "equal"
39
+ | "nequal"
40
+ | "in_range"
41
+ | "not_in_range"
42
+ | "crosses"
43
+ | "crosses_above"
44
+ | "crosses_below"
45
+ | "above%"
46
+ | "below%"
47
+ | "match"
48
+ | "nmatch"
49
+ | "has"
50
+ | "has_none_of"
51
+ | "empty"
52
+ | "nempty";
53
+
54
+ export interface ScreenFilterClause {
55
+ field: string;
56
+ op: ScreenFilterOp;
57
+ value?: unknown;
58
+ }
59
+
60
+ export interface ScreenSort {
61
+ field: string;
62
+ direction?: "asc" | "desc";
63
+ }
64
+
65
+ export interface ScreenStocksOpts {
66
+ market?: string;
67
+ columns?: string[];
68
+ filter?: ScreenFilterClause[];
69
+ sort?: ScreenSort;
70
+ limit?: number;
71
+ }
72
+
73
+ export interface ScreenerRow {
74
+ tvSymbol: string;
75
+ symbol: string;
76
+ values: Record<string, unknown | null>;
77
+ sourceProvider: "tradingview";
78
+ dataCaveat: string;
79
+ }
80
+
81
+ export interface TradingViewQuote {
82
+ requestedSymbol: string;
83
+ tvSymbol: string;
84
+ symbol: string;
85
+ price: number;
86
+ change: number;
87
+ changePercent: number;
88
+ volume: number;
89
+ exchange?: string;
90
+ market?: string;
91
+ name?: string;
92
+ type?: string;
93
+ typespecs?: unknown;
94
+ sourceProvider: "tradingview";
95
+ dataCaveat: string;
96
+ }
97
+
98
+ interface TradingViewResponse {
99
+ fields?: string[];
100
+ symbols?: Array<{
101
+ s: string;
102
+ f: unknown[];
103
+ }>;
104
+ }
105
+
106
+ interface ScannerBody {
107
+ markets?: string[];
108
+ symbols?: { tickers?: string[]; query?: { types: string[] } };
109
+ columns: readonly string[];
110
+ filter?: Array<{ left: string; operation: string; right?: unknown }>;
111
+ sort?: { sortBy: string; sortOrder: "asc" | "desc" };
112
+ range: [number, number];
113
+ options: { lang: "en" };
114
+ }
115
+
116
+ interface DecodedRow {
117
+ tvSymbol: string;
118
+ symbol: string;
119
+ values: Record<string, unknown | null>;
120
+ }
121
+
122
+ export function buildTvSymbol(exchange: string, ticker: string): string {
123
+ const trimmedTicker = ticker.trim().toUpperCase();
124
+ if (trimmedTicker.includes(":")) return trimmedTicker;
125
+ const trimmedExchange = exchange.trim().toUpperCase();
126
+ return trimmedExchange ? `${trimmedExchange}:${trimmedTicker}` : trimmedTicker;
127
+ }
128
+
129
+ export async function screenStocks(opts: ScreenStocksOpts = {}): Promise<ScreenerRow[]> {
130
+ const market = opts.market?.trim().toLowerCase() || "america";
131
+ const columns = opts.columns?.length ? opts.columns : [...DEFAULT_COLUMNS];
132
+ const body = buildScannerBody({
133
+ market,
134
+ columns,
135
+ filter: opts.filter,
136
+ sort: opts.sort,
137
+ limit: opts.limit,
138
+ });
139
+ const rows = decodeScannerRows(await scannerFetch(market, body), columns);
140
+ return rows.map((row) => ({
141
+ ...row,
142
+ sourceProvider: "tradingview" as const,
143
+ dataCaveat: DATA_CAVEAT,
144
+ }));
145
+ }
146
+
147
+ export async function getQuotes(symbols: string[]): Promise<TradingViewQuote[]> {
148
+ const requested = symbols.map(normalizeRequestedSymbol).filter(Boolean);
149
+ const qualified = requested.filter(isTradingViewQualified);
150
+ const bare = requested.filter(
151
+ (symbol) => !isTradingViewQualified(symbol) && !shouldSkipTradingView(symbol),
152
+ );
153
+
154
+ const resolved = new Map<string, TradingViewQuote>();
155
+
156
+ if (qualified.length > 0) {
157
+ const body = buildQualifiedQuoteBody(qualified);
158
+ const rows = decodeScannerRows(await scannerFetch("global", body), [...QUOTE_COLUMNS]);
159
+ const byTvSymbol = new Map(rows.map((row) => [row.tvSymbol.toUpperCase(), row]));
160
+ for (const symbol of qualified) {
161
+ const row = byTvSymbol.get(symbol);
162
+ const quote = row ? rowToQuote(symbol, row) : undefined;
163
+ if (quote) resolved.set(symbol, quote);
164
+ }
165
+ }
166
+
167
+ if (bare.length > 0) {
168
+ const body = buildBareQuoteBody(bare);
169
+ const rows = decodeScannerRows(await scannerFetch("america", body), [...QUOTE_COLUMNS]);
170
+ const byName = groupRowsByName(rows);
171
+ for (const symbol of bare) {
172
+ const row = pickPrimaryListing(byName.get(symbol) ?? []);
173
+ const quote = row ? rowToQuote(symbol, row) : undefined;
174
+ if (quote) resolved.set(symbol, quote);
175
+ }
176
+ }
177
+
178
+ return requested.flatMap((symbol) => {
179
+ const quote = resolved.get(symbol);
180
+ return quote ? [quote] : [];
181
+ });
182
+ }
183
+
184
+ function buildScannerBody(
185
+ opts: Required<Pick<ScreenStocksOpts, "market" | "columns">> &
186
+ Pick<ScreenStocksOpts, "filter" | "sort" | "limit">,
187
+ ): ScannerBody {
188
+ const limit = clampLimit(opts.limit);
189
+ return {
190
+ markets: [opts.market],
191
+ columns: opts.columns,
192
+ ...(opts.filter?.length && { filter: opts.filter.map(mapFilterClause) }),
193
+ ...(opts.sort && {
194
+ sort: {
195
+ sortBy: opts.sort.field,
196
+ sortOrder: opts.sort.direction ?? "desc",
197
+ },
198
+ }),
199
+ range: [0, limit],
200
+ options: { lang: "en" },
201
+ };
202
+ }
203
+
204
+ function buildQualifiedQuoteBody(tickers: string[]): ScannerBody {
205
+ return {
206
+ symbols: { tickers },
207
+ columns: QUOTE_COLUMNS,
208
+ range: [0, clampLimit(tickers.length)],
209
+ options: { lang: "en" },
210
+ };
211
+ }
212
+
213
+ function buildBareQuoteBody(symbols: string[]): ScannerBody {
214
+ return {
215
+ markets: ["america"],
216
+ symbols: { query: { types: [] } },
217
+ columns: QUOTE_COLUMNS,
218
+ filter: [
219
+ { left: "name", operation: "in_range", right: symbols },
220
+ { left: "is_primary", operation: "equal", right: true },
221
+ { left: "type", operation: "in_range", right: ["stock", "fund", "dr"] },
222
+ ],
223
+ range: [0, 500],
224
+ options: { lang: "en" },
225
+ };
226
+ }
227
+
228
+ function mapFilterClause(clause: ScreenFilterClause): {
229
+ left: string;
230
+ operation: string;
231
+ right?: unknown;
232
+ } {
233
+ return {
234
+ left: clause.field,
235
+ operation: clause.op,
236
+ ...(clause.value !== undefined && { right: clause.value }),
237
+ };
238
+ }
239
+
240
+ async function scannerFetch(market: string, body: ScannerBody): Promise<TradingViewResponse> {
241
+ const endpoint = `${BASE_URL}/${market}/scan2?label-product=screener-stock`;
242
+ const cacheKey = `tradingview:scan2:${market}:${stableStringify(body)}`;
243
+ const cached = cache.get<TradingViewResponse>(cacheKey);
244
+ if (cached) return cached;
245
+
246
+ try {
247
+ await rateLimiter.acquire("tradingview");
248
+ const response = await httpPost<TradingViewResponse>(endpoint, body, {
249
+ headers: {
250
+ Origin: "https://www.tradingview.com",
251
+ Referer: "https://www.tradingview.com/",
252
+ "User-Agent": BROWSER_UA,
253
+ },
254
+ });
255
+ cache.set(cacheKey, response, TTL.SCREENER);
256
+ return response;
257
+ } catch (error) {
258
+ const stale = cache.getStale<TradingViewResponse>(cacheKey, STALE_LIMIT.SCREENER);
259
+ if (stale) return stale.value;
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ function decodeScannerRows(
265
+ response: TradingViewResponse,
266
+ requestedColumns: readonly string[],
267
+ ): DecodedRow[] {
268
+ const fields = response.fields ?? [];
269
+ return (response.symbols ?? []).map((row) => {
270
+ const values: Record<string, unknown | null> = {};
271
+ for (const column of requestedColumns) {
272
+ const index = fields.indexOf(column);
273
+ values[column] = index >= 0 ? (row.f[index] ?? null) : null;
274
+ }
275
+ return {
276
+ tvSymbol: row.s,
277
+ symbol: stringValue(values.name) ?? symbolFromTvSymbol(row.s),
278
+ values,
279
+ };
280
+ });
281
+ }
282
+
283
+ function rowToQuote(requestedSymbol: string, row: DecodedRow): TradingViewQuote | undefined {
284
+ const price = numberValue(row.values.close);
285
+ if (price === undefined) return undefined;
286
+ const changePercent = numberValue(row.values.change) ?? 0;
287
+ const changeAbs = numberValue(row.values.change_abs) ?? 0;
288
+ return {
289
+ requestedSymbol,
290
+ tvSymbol: row.tvSymbol,
291
+ symbol: row.symbol,
292
+ price,
293
+ change: changeAbs,
294
+ changePercent,
295
+ volume: numberValue(row.values.volume) ?? 0,
296
+ exchange: stringValue(row.values.exchange),
297
+ market: stringValue(row.values.market),
298
+ name: stringValue(row.values.description),
299
+ type: stringValue(row.values.type),
300
+ typespecs: row.values.typespecs ?? undefined,
301
+ sourceProvider: "tradingview",
302
+ dataCaveat: DATA_CAVEAT,
303
+ };
304
+ }
305
+
306
+ function groupRowsByName(rows: DecodedRow[]): Map<string, DecodedRow[]> {
307
+ const grouped = new Map<string, DecodedRow[]>();
308
+ for (const row of rows) {
309
+ const name = row.symbol.toUpperCase();
310
+ grouped.set(name, [...(grouped.get(name) ?? []), row]);
311
+ }
312
+ return grouped;
313
+ }
314
+
315
+ function pickPrimaryListing(rows: DecodedRow[]): DecodedRow | undefined {
316
+ if (rows.length === 0) return undefined;
317
+ return [...rows].sort(compareListings)[0];
318
+ }
319
+
320
+ function compareListings(a: DecodedRow, b: DecodedRow): number {
321
+ return (
322
+ scoreMarket(a) - scoreMarket(b) ||
323
+ scoreType(a) - scoreType(b) ||
324
+ scoreExchange(a) - scoreExchange(b) ||
325
+ a.tvSymbol.localeCompare(b.tvSymbol)
326
+ );
327
+ }
328
+
329
+ function scoreMarket(row: DecodedRow): number {
330
+ return stringValue(row.values.market)?.toLowerCase() === "america" ? 0 : 1;
331
+ }
332
+
333
+ function scoreType(row: DecodedRow): number {
334
+ const type = stringValue(row.values.type)?.toLowerCase();
335
+ if (type === "stock") return 0;
336
+ if (type === "fund") return 1;
337
+ if (type === "dr") return 2;
338
+ return 3;
339
+ }
340
+
341
+ function scoreExchange(row: DecodedRow): number {
342
+ const exchange = stringValue(row.values.exchange)?.toUpperCase();
343
+ if (exchange === "NASDAQ") return 0;
344
+ if (exchange === "NYSE") return 1;
345
+ if (exchange === "AMEX") return 2;
346
+ return 3;
347
+ }
348
+
349
+ function clampLimit(limit: number | undefined): number {
350
+ if (limit == null) return 50;
351
+ if (limit < 1) return 50;
352
+ return Math.min(Math.floor(limit), 500);
353
+ }
354
+
355
+ function normalizeRequestedSymbol(symbol: string): string {
356
+ return symbol.trim().toUpperCase();
357
+ }
358
+
359
+ export function isTradingViewQualified(symbol: string): boolean {
360
+ return /^[A-Z0-9_]+:[A-Z0-9._-]+$/.test(symbol);
361
+ }
362
+
363
+ export function shouldSkipTradingView(symbol: string): boolean {
364
+ return /(?:-USD|\.(?:TO|DE|T|L|HK))$/i.test(symbol);
365
+ }
366
+
367
+ export function canUseTradingViewQuote(symbol: string): boolean {
368
+ const normalized = normalizeRequestedSymbol(symbol);
369
+ return (
370
+ normalized.length > 0 &&
371
+ (isTradingViewQualified(normalized) || !shouldSkipTradingView(normalized))
372
+ );
373
+ }
374
+
375
+ function symbolFromTvSymbol(tvSymbol: string): string {
376
+ return tvSymbol.includes(":") ? tvSymbol.split(":").at(-1)! : tvSymbol;
377
+ }
378
+
379
+ function stringValue(value: unknown): string | undefined {
380
+ return typeof value === "string" ? value : undefined;
381
+ }
382
+
383
+ function numberValue(value: unknown): number | undefined {
384
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
385
+ }
386
+
387
+ function stableStringify(value: unknown): string {
388
+ return JSON.stringify(sortObjectKeys(value));
389
+ }
390
+
391
+ function sortObjectKeys(value: unknown): unknown {
392
+ if (Array.isArray(value)) return value.map(sortObjectKeys);
393
+ if (!value || typeof value !== "object") return value;
394
+ return Object.fromEntries(
395
+ Object.entries(value)
396
+ .sort(([a], [b]) => a.localeCompare(b))
397
+ .map(([key, nested]) => [key, sortObjectKeys(nested)]),
398
+ );
399
+ }
@@ -1,10 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import Database from "better-sqlite3";
4
3
  import { Scraper, SearchMode } from "@the-convocation/twitter-scraper";
5
- import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
6
- import { rateLimiter } from "../infra/rate-limiter.js";
4
+ import Database from "better-sqlite3";
5
+ import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
7
6
  import { getBrowserProfileDir } from "../infra/opencandle-paths.js";
7
+ import { rateLimiter } from "../infra/rate-limiter.js";
8
8
  import type { TwitterSentimentResult, TwitterTweet } from "../types/sentiment.js";
9
9
 
10
10
  // ── Cookie extraction ────────────────────────────────────
@@ -35,7 +35,7 @@ export function readTwitterCookies(profileDir: string): FirefoxCookie[] {
35
35
 
36
36
  // ── Sentiment scoring ────────────────────────────────────
37
37
 
38
- import { BULLISH_TERMS, BEARISH_TERMS } from "../sentiment/keywords.js";
38
+ import { BEARISH_TERMS, BULLISH_TERMS } from "../sentiment/keywords.js";
39
39
 
40
40
  export function scoreTwitterSentiment(
41
41
  tweets: Array<{ text: string; likes: number; retweets: number }>,
@@ -68,7 +68,7 @@ export function scoreTwitterSentiment(
68
68
  // ── Query normalization ──────────────────────────────────
69
69
 
70
70
  export function normalizeQuery(query: string): string {
71
- if (/^[A-Z]{1,5}$/.test(query)) return "$" + query;
71
+ if (/^[A-Z]{1,5}$/.test(query)) return `$${query}`;
72
72
  return query;
73
73
  }
74
74
 
@@ -135,9 +135,7 @@ export async function getTwitterSentiment(
135
135
  const tickerRegex = /\$([A-Z]{1,5})\b/g;
136
136
  const mentionCounts = new Map<string, number>();
137
137
  // Exclude the searched ticker itself from co-mentions
138
- const searchedTicker = normalizedQuery.startsWith("$")
139
- ? normalizedQuery.slice(1)
140
- : null;
138
+ const searchedTicker = normalizedQuery.startsWith("$") ? normalizedQuery.slice(1) : null;
141
139
  for (const t of tweets) {
142
140
  for (const match of t.text.matchAll(tickerRegex)) {
143
141
  const ticker = match[1];
@@ -1,15 +1,14 @@
1
- import { search, searchNews, SafeSearchType, SearchTimeType } from "duck-duck-scrape";
2
- import type { SearchResult } from "duck-duck-scrape";
3
- import type { NewsResult } from "duck-duck-scrape";
4
- import { httpGet, HttpError } from "../infra/http-client.js";
5
- import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
6
- import { rateLimiter } from "../infra/rate-limiter.js";
1
+ import type { NewsResult, SearchResult } from "duck-duck-scrape";
2
+ import { SafeSearchType, SearchTimeType, search, searchNews } from "duck-duck-scrape";
7
3
  import { getConfig } from "../config.js";
8
- import { withFallback } from "./with-fallback.js";
4
+ import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
5
+ import { HttpError, httpGet } from "../infra/http-client.js";
6
+ import { rateLimiter } from "../infra/rate-limiter.js";
7
+ import type { ProviderResult } from "../runtime/evidence.js";
8
+ import type { WebSearchEnvelope, WebSearchResult } from "../types/sentiment.js";
9
9
  import { exaSearch } from "./exa-search.js";
10
10
  import { ProviderCredentialError } from "./provider-credential-error.js";
11
- import type { ProviderResult } from "../runtime/evidence.js";
12
- import type { WebSearchResult, WebSearchEnvelope } from "../types/sentiment.js";
11
+ import { withFallback } from "./with-fallback.js";
13
12
 
14
13
  export interface WebSearchOpts {
15
14
  category: "news" | "general";
@@ -85,10 +84,7 @@ function ddgCacheKey(query: string, opts: WebSearchOpts): string {
85
84
  return `web:ddg:${query}:${opts.category}:${opts.freshness}:${opts.limit}`;
86
85
  }
87
86
 
88
- export async function ddgSearch(
89
- query: string,
90
- opts: WebSearchOpts,
91
- ): Promise<WebSearchEnvelope> {
87
+ export async function ddgSearch(query: string, opts: WebSearchOpts): Promise<WebSearchEnvelope> {
92
88
  const key = ddgCacheKey(query, opts);
93
89
  const cached = cache.get<WebSearchEnvelope>(key);
94
90
  if (cached) return cached;
@@ -139,10 +135,14 @@ const BRAVE_BASE = "https://api.search.brave.com/res/v1";
139
135
 
140
136
  function mapBraveFreshness(freshness: WebSearchOpts["freshness"]): string {
141
137
  switch (freshness) {
142
- case "hours": return "ph";
143
- case "day": return "pd";
144
- case "week": return "pw";
145
- case "month": return "pm";
138
+ case "hours":
139
+ return "ph";
140
+ case "day":
141
+ return "pd";
142
+ case "week":
143
+ return "pw";
144
+ case "month":
145
+ return "pm";
146
146
  }
147
147
  }
148
148
 
@@ -271,9 +271,16 @@ export async function searchWeb(
271
271
  break;
272
272
  case "brave":
273
273
  if (!config.braveApiKey) {
274
- return { status: "unavailable", reason: "BRAVE_API_KEY not configured", provider: "brave_search" };
274
+ return {
275
+ status: "unavailable",
276
+ reason: "BRAVE_API_KEY not configured",
277
+ provider: "brave_search",
278
+ };
275
279
  }
276
- entries.push({ provider: "brave_search", fn: () => braveSearch(normalized, resolved, config.braveApiKey!) });
280
+ entries.push({
281
+ provider: "brave_search",
282
+ fn: () => braveSearch(normalized, resolved, config.braveApiKey!),
283
+ });
277
284
  break;
278
285
  case "ddg":
279
286
  entries.push({ provider: "ddg", fn: () => ddgSearch(normalized, resolved) });
@@ -285,7 +292,10 @@ export async function searchWeb(
285
292
  // Default cascade: Exa → Brave → DDG
286
293
  entries.push({ provider: "exa", fn: () => exaSearch(normalized, resolved) });
287
294
  if (config.braveApiKey) {
288
- entries.push({ provider: "brave_search", fn: () => braveSearch(normalized, resolved, config.braveApiKey!) });
295
+ entries.push({
296
+ provider: "brave_search",
297
+ fn: () => braveSearch(normalized, resolved, config.braveApiKey!),
298
+ });
289
299
  }
290
300
  entries.push({ provider: "ddg", fn: () => ddgSearch(normalized, resolved) });
291
301
 
@@ -1,6 +1,6 @@
1
1
  import type { ProviderResult } from "../runtime/evidence.js";
2
- import { wrapProvider } from "./wrap-provider.js";
3
2
  import { getProviderTracker } from "../runtime/run-context.js";
3
+ import { wrapProvider } from "./wrap-provider.js";
4
4
 
5
5
  export interface FallbackEntry<T> {
6
6
  provider: string;
@@ -16,11 +16,10 @@ export interface FallbackEntry<T> {
16
16
  * provider function (Level B). By the time a provider throws to wrapProvider,
17
17
  * it already tried its own stale cache.
18
18
  */
19
- export async function withFallback<T>(
20
- entries: FallbackEntry<T>[],
21
- ): Promise<ProviderResult<T>> {
19
+ export async function withFallback<T>(entries: FallbackEntry<T>[]): Promise<ProviderResult<T>> {
22
20
  const tracker = getProviderTracker();
23
21
  const attempted: string[] = [];
22
+ const failures: string[] = [];
24
23
 
25
24
  for (const entry of entries) {
26
25
  if (tracker?.isCircuitOpen(entry.provider)) continue;
@@ -28,14 +27,16 @@ export async function withFallback<T>(
28
27
 
29
28
  const result = await wrapProvider(entry.provider, entry.fn);
30
29
  if (result.status === "ok") return result;
30
+ failures.push(`${entry.provider}: ${result.reason}`);
31
31
  // wrapProvider already called recordFailure on the tracker
32
32
  }
33
33
 
34
34
  return {
35
35
  status: "unavailable",
36
- reason: attempted.length > 0
37
- ? `all providers failed: ${attempted.join(", ")}`
38
- : `all providers circuit-open: ${entries.map((e) => e.provider).join(", ")}`,
36
+ reason:
37
+ attempted.length > 0
38
+ ? `all providers failed: ${failures.join("; ")}`
39
+ : `all providers circuit-open: ${entries.map((e) => e.provider).join(", ")}`,
39
40
  provider: entries[0]?.provider ?? "unknown",
40
41
  };
41
42
  }
@@ -1,6 +1,7 @@
1
+ import { runWithStaleMetadata } from "../infra/cache.js";
1
2
  import type { ProviderResult } from "../runtime/evidence.js";
2
3
  import { getProviderTracker } from "../runtime/run-context.js";
3
- import { cache } from "../infra/cache.js";
4
+ import { InvalidSymbolError } from "./errors.js";
4
5
  import { ProviderCredentialError } from "./provider-credential-error.js";
5
6
 
6
7
  /**
@@ -17,9 +18,8 @@ import { ProviderCredentialError } from "./provider-credential-error.js";
17
18
  * When a run context is active, checks circuit breaker state before
18
19
  * calling and records failures after.
19
20
  *
20
- * After a successful provider call, checks if the cache's stale flag
21
- * was set (meaning the provider fell back to stale cached data internally)
22
- * and propagates that metadata on the result.
21
+ * After a successful provider call, propagates stale cache metadata recorded
22
+ * in the current provider call's async scope.
23
23
  */
24
24
  export async function wrapProvider<T>(
25
25
  providerId: string,
@@ -36,13 +36,12 @@ export async function wrapProvider<T>(
36
36
  }
37
37
 
38
38
  try {
39
- const data = await fn();
40
- const { stale, cachedAt } = cache.consumeStaleFlag();
39
+ const { value: data, stale } = await runWithStaleMetadata(fn);
41
40
  return {
42
41
  status: "ok",
43
42
  data,
44
- timestamp: stale ? new Date(cachedAt).toISOString() : new Date().toISOString(),
45
- stale: stale || undefined,
43
+ timestamp: stale ? new Date(stale.cachedAt).toISOString() : new Date().toISOString(),
44
+ stale: stale ? true : undefined,
46
45
  };
47
46
  } catch (error) {
48
47
  // Credential errors are re-thrown so the tool-layer `withCredentialCheck`
@@ -52,9 +51,15 @@ export async function wrapProvider<T>(
52
51
  if (error instanceof ProviderCredentialError) {
53
52
  throw error;
54
53
  }
54
+ if (error instanceof InvalidSymbolError) {
55
+ return {
56
+ status: "unavailable",
57
+ reason: error.message,
58
+ provider: providerId,
59
+ };
60
+ }
55
61
  tracker?.recordFailure(providerId);
56
- const reason =
57
- error instanceof Error ? error.message : "unknown_error";
62
+ const reason = error instanceof Error ? error.message : "unknown_error";
58
63
  return {
59
64
  status: "unavailable",
60
65
  reason,