opencandle 0.4.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 (564) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +186 -117
  3. package/dist/analysts/contracts.d.ts +1 -3
  4. package/dist/analysts/contracts.js +1 -11
  5. package/dist/analysts/contracts.js.map +1 -1
  6. package/dist/analysts/orchestrator.d.ts +1 -3
  7. package/dist/analysts/orchestrator.js +1 -26
  8. package/dist/analysts/orchestrator.js.map +1 -1
  9. package/dist/cli.js +32 -8
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.d.ts +19 -3
  12. package/dist/config.js +69 -3
  13. package/dist/config.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/infra/browser.d.ts +1 -3
  18. package/dist/infra/browser.js +4 -2
  19. package/dist/infra/browser.js.map +1 -1
  20. package/dist/infra/cache.d.ts +8 -11
  21. package/dist/infra/cache.js +17 -15
  22. package/dist/infra/cache.js.map +1 -1
  23. package/dist/infra/http-client.d.ts +4 -1
  24. package/dist/infra/http-client.js +59 -6
  25. package/dist/infra/http-client.js.map +1 -1
  26. package/dist/infra/index.d.ts +3 -3
  27. package/dist/infra/index.js +3 -3
  28. package/dist/infra/index.js.map +1 -1
  29. package/dist/infra/native-dependencies.js +2 -2
  30. package/dist/infra/native-dependencies.js.map +1 -1
  31. package/dist/infra/node-version.js.map +1 -1
  32. package/dist/infra/opencandle-paths.d.ts +0 -3
  33. package/dist/infra/opencandle-paths.js +4 -11
  34. package/dist/infra/opencandle-paths.js.map +1 -1
  35. package/dist/infra/rate-limiter.d.ts +4 -0
  36. package/dist/infra/rate-limiter.js +17 -10
  37. package/dist/infra/rate-limiter.js.map +1 -1
  38. package/dist/market-state/alert-conditions.d.ts +34 -0
  39. package/dist/market-state/alert-conditions.js +23 -0
  40. package/dist/market-state/alert-conditions.js.map +1 -0
  41. package/dist/market-state/alert-runner.d.ts +55 -0
  42. package/dist/market-state/alert-runner.js +634 -0
  43. package/dist/market-state/alert-runner.js.map +1 -0
  44. package/dist/market-state/daily-report.d.ts +26 -0
  45. package/dist/market-state/daily-report.js +179 -0
  46. package/dist/market-state/daily-report.js.map +1 -0
  47. package/dist/market-state/local-automation-service.d.ts +25 -0
  48. package/dist/market-state/local-automation-service.js +119 -0
  49. package/dist/market-state/local-automation-service.js.map +1 -0
  50. package/dist/market-state/notification-delivery.d.ts +14 -0
  51. package/dist/market-state/notification-delivery.js +139 -0
  52. package/dist/market-state/notification-delivery.js.map +1 -0
  53. package/dist/market-state/resolve-for-mutation.d.ts +10 -0
  54. package/dist/market-state/resolve-for-mutation.js +15 -0
  55. package/dist/market-state/resolve-for-mutation.js.map +1 -0
  56. package/dist/market-state/resolve.d.ts +14 -0
  57. package/dist/market-state/resolve.js +89 -0
  58. package/dist/market-state/resolve.js.map +1 -0
  59. package/dist/market-state/service.d.ts +527 -0
  60. package/dist/market-state/service.js +1099 -0
  61. package/dist/market-state/service.js.map +1 -0
  62. package/dist/memory/index.d.ts +7 -7
  63. package/dist/memory/index.js +6 -6
  64. package/dist/memory/index.js.map +1 -1
  65. package/dist/memory/manager.d.ts +9 -0
  66. package/dist/memory/manager.js +39 -22
  67. package/dist/memory/manager.js.map +1 -1
  68. package/dist/memory/retrieval.js +7 -4
  69. package/dist/memory/retrieval.js.map +1 -1
  70. package/dist/memory/sqlite.js +385 -3
  71. package/dist/memory/sqlite.js.map +1 -1
  72. package/dist/memory/storage.d.ts +3 -2
  73. package/dist/memory/storage.js +1 -2
  74. package/dist/memory/storage.js.map +1 -1
  75. package/dist/memory/tool-defaults.js +64 -28
  76. package/dist/memory/tool-defaults.js.map +1 -1
  77. package/dist/memory/types.js +4 -0
  78. package/dist/memory/types.js.map +1 -1
  79. package/dist/monitor.d.ts +2 -0
  80. package/dist/monitor.js +104 -0
  81. package/dist/monitor.js.map +1 -0
  82. package/dist/onboarding/connect.js +4 -6
  83. package/dist/onboarding/connect.js.map +1 -1
  84. package/dist/onboarding/credential-interceptor.js +1 -1
  85. package/dist/onboarding/credential-interceptor.js.map +1 -1
  86. package/dist/onboarding/degradation-accumulator.js +1 -3
  87. package/dist/onboarding/degradation-accumulator.js.map +1 -1
  88. package/dist/onboarding/providers.js +3 -16
  89. package/dist/onboarding/providers.js.map +1 -1
  90. package/dist/onboarding/state.js.map +1 -1
  91. package/dist/onboarding/tool-helpers.js +1 -1
  92. package/dist/onboarding/tool-helpers.js.map +1 -1
  93. package/dist/onboarding/tool-tags.js +6 -4
  94. package/dist/onboarding/tool-tags.js.map +1 -1
  95. package/dist/onboarding/validation.js +1 -1
  96. package/dist/onboarding/validation.js.map +1 -1
  97. package/dist/pi/opencandle-extension.d.ts +8 -0
  98. package/dist/pi/opencandle-extension.js +637 -59
  99. package/dist/pi/opencandle-extension.js.map +1 -1
  100. package/dist/pi/session.d.ts +1 -1
  101. package/dist/pi/session.js +3 -1
  102. package/dist/pi/session.js.map +1 -1
  103. package/dist/pi/setup.js +17 -2
  104. package/dist/pi/setup.js.map +1 -1
  105. package/dist/pi/tool-adapter.js +5 -2
  106. package/dist/pi/tool-adapter.js.map +1 -1
  107. package/dist/prompts/context-builder.d.ts +18 -3
  108. package/dist/prompts/context-builder.js +117 -18
  109. package/dist/prompts/context-builder.js.map +1 -1
  110. package/dist/prompts/disclaimer.js +1 -1
  111. package/dist/prompts/disclaimer.js.map +1 -1
  112. package/dist/prompts/policy-cards.d.ts +13 -0
  113. package/dist/prompts/policy-cards.js +197 -0
  114. package/dist/prompts/policy-cards.js.map +1 -0
  115. package/dist/prompts/sections.d.ts +1 -1
  116. package/dist/prompts/sections.js +3 -3
  117. package/dist/prompts/sections.js.map +1 -1
  118. package/dist/prompts/symbol-preflight.d.ts +20 -0
  119. package/dist/prompts/symbol-preflight.js +49 -0
  120. package/dist/prompts/symbol-preflight.js.map +1 -0
  121. package/dist/prompts/workflow-prompts.d.ts +1 -1
  122. package/dist/prompts/workflow-prompts.js +209 -19
  123. package/dist/prompts/workflow-prompts.js.map +1 -1
  124. package/dist/providers/alpha-vantage.d.ts +1 -1
  125. package/dist/providers/alpha-vantage.js +49 -8
  126. package/dist/providers/alpha-vantage.js.map +1 -1
  127. package/dist/providers/coingecko.js +1 -1
  128. package/dist/providers/coingecko.js.map +1 -1
  129. package/dist/providers/errors.d.ts +5 -0
  130. package/dist/providers/errors.js +11 -0
  131. package/dist/providers/errors.js.map +1 -0
  132. package/dist/providers/exa-search.d.ts +2 -2
  133. package/dist/providers/exa-search.js +19 -11
  134. package/dist/providers/exa-search.js.map +1 -1
  135. package/dist/providers/fear-greed.js +1 -1
  136. package/dist/providers/fear-greed.js.map +1 -1
  137. package/dist/providers/finnhub.js +3 -5
  138. package/dist/providers/finnhub.js.map +1 -1
  139. package/dist/providers/fred.js +2 -2
  140. package/dist/providers/fred.js.map +1 -1
  141. package/dist/providers/index.d.ts +7 -6
  142. package/dist/providers/index.js +6 -5
  143. package/dist/providers/index.js.map +1 -1
  144. package/dist/providers/reddit.js +2 -2
  145. package/dist/providers/reddit.js.map +1 -1
  146. package/dist/providers/sec-edgar.d.ts +9 -1
  147. package/dist/providers/sec-edgar.js +181 -6
  148. package/dist/providers/sec-edgar.js.map +1 -1
  149. package/dist/providers/tradingview.d.ts +47 -0
  150. package/dist/providers/tradingview.js +275 -0
  151. package/dist/providers/tradingview.js.map +1 -0
  152. package/dist/providers/twitter.js +6 -8
  153. package/dist/providers/twitter.js.map +1 -1
  154. package/dist/providers/web-search.js +26 -12
  155. package/dist/providers/web-search.js.map +1 -1
  156. package/dist/providers/with-fallback.js +4 -2
  157. package/dist/providers/with-fallback.js.map +1 -1
  158. package/dist/providers/wrap-provider.d.ts +2 -3
  159. package/dist/providers/wrap-provider.js +14 -8
  160. package/dist/providers/wrap-provider.js.map +1 -1
  161. package/dist/providers/yahoo-finance.d.ts +3 -1
  162. package/dist/providers/yahoo-finance.js +226 -11
  163. package/dist/providers/yahoo-finance.js.map +1 -1
  164. package/dist/routing/classify-intent.d.ts +9 -0
  165. package/dist/routing/classify-intent.js +153 -3
  166. package/dist/routing/classify-intent.js.map +1 -1
  167. package/dist/routing/defaults.d.ts +1 -1
  168. package/dist/routing/defaults.js +3 -3
  169. package/dist/routing/defaults.js.map +1 -1
  170. package/dist/routing/entity-extractor.d.ts +2 -0
  171. package/dist/routing/entity-extractor.js +377 -26
  172. package/dist/routing/entity-extractor.js.map +1 -1
  173. package/dist/routing/fund-symbols.d.ts +2 -0
  174. package/dist/routing/fund-symbols.js +55 -0
  175. package/dist/routing/fund-symbols.js.map +1 -0
  176. package/dist/routing/horizon.d.ts +1 -0
  177. package/dist/routing/horizon.js +10 -0
  178. package/dist/routing/horizon.js.map +1 -0
  179. package/dist/routing/index.d.ts +12 -6
  180. package/dist/routing/index.js +8 -4
  181. package/dist/routing/index.js.map +1 -1
  182. package/dist/routing/legacy-rule-router.d.ts +9 -0
  183. package/dist/routing/legacy-rule-router.js +12 -0
  184. package/dist/routing/legacy-rule-router.js.map +1 -0
  185. package/dist/routing/planning.d.ts +54 -0
  186. package/dist/routing/planning.js +562 -0
  187. package/dist/routing/planning.js.map +1 -0
  188. package/dist/routing/route-manifest.d.ts +35 -0
  189. package/dist/routing/route-manifest.js +242 -0
  190. package/dist/routing/route-manifest.js.map +1 -0
  191. package/dist/routing/router-llm-client.js.map +1 -1
  192. package/dist/routing/router-prompt.js +46 -45
  193. package/dist/routing/router-prompt.js.map +1 -1
  194. package/dist/routing/router-types.d.ts +10 -0
  195. package/dist/routing/router.d.ts +1 -0
  196. package/dist/routing/router.js +572 -13
  197. package/dist/routing/router.js.map +1 -1
  198. package/dist/routing/slot-resolver.d.ts +1 -1
  199. package/dist/routing/slot-resolver.js +45 -7
  200. package/dist/routing/slot-resolver.js.map +1 -1
  201. package/dist/routing/symbol-disambiguator.d.ts +11 -0
  202. package/dist/routing/symbol-disambiguator.js +52 -0
  203. package/dist/routing/symbol-disambiguator.js.map +1 -0
  204. package/dist/routing/turn-context.d.ts +44 -0
  205. package/dist/routing/turn-context.js +45 -0
  206. package/dist/routing/turn-context.js.map +1 -0
  207. package/dist/routing/types.d.ts +15 -1
  208. package/dist/runtime/answer-contracts.d.ts +82 -0
  209. package/dist/runtime/answer-contracts.js +442 -0
  210. package/dist/runtime/answer-contracts.js.map +1 -0
  211. package/dist/runtime/artifact-contracts.d.ts +14 -0
  212. package/dist/runtime/artifact-contracts.js +57 -0
  213. package/dist/runtime/artifact-contracts.js.map +1 -0
  214. package/dist/runtime/planning-evidence.d.ts +99 -0
  215. package/dist/runtime/planning-evidence.js +466 -0
  216. package/dist/runtime/planning-evidence.js.map +1 -0
  217. package/dist/runtime/prompt-step.d.ts +1 -9
  218. package/dist/runtime/prompt-step.js +0 -10
  219. package/dist/runtime/prompt-step.js.map +1 -1
  220. package/dist/runtime/run-context.d.ts +5 -2
  221. package/dist/runtime/run-context.js +8 -1
  222. package/dist/runtime/run-context.js.map +1 -1
  223. package/dist/runtime/session-coordinator.d.ts +29 -3
  224. package/dist/runtime/session-coordinator.js +204 -31
  225. package/dist/runtime/session-coordinator.js.map +1 -1
  226. package/dist/runtime/session-title.d.ts +14 -0
  227. package/dist/runtime/session-title.js +50 -0
  228. package/dist/runtime/session-title.js.map +1 -0
  229. package/dist/runtime/tool-defaults-wrapper.js +1 -3
  230. package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
  231. package/dist/runtime/validation.js.map +1 -1
  232. package/dist/runtime/workflow-events.js.map +1 -1
  233. package/dist/runtime/workflow-runner.d.ts +3 -3
  234. package/dist/runtime/workflow-runner.js +1 -1
  235. package/dist/runtime/workflow-runner.js.map +1 -1
  236. package/dist/sentiment/adapters/finnhub.d.ts +1 -1
  237. package/dist/sentiment/adapters/finnhub.js +6 -1
  238. package/dist/sentiment/adapters/finnhub.js.map +1 -1
  239. package/dist/sentiment/adapters/reddit.d.ts +2 -2
  240. package/dist/sentiment/adapters/twitter.d.ts +1 -1
  241. package/dist/sentiment/adapters/web.d.ts +1 -1
  242. package/dist/sentiment/index.d.ts +9 -11
  243. package/dist/sentiment/index.js +9 -20
  244. package/dist/sentiment/index.js.map +1 -1
  245. package/dist/sentiment/keywords.js +26 -4
  246. package/dist/sentiment/keywords.js.map +1 -1
  247. package/dist/sentiment/pipeline.d.ts +2 -2
  248. package/dist/sentiment/pipeline.js +1 -1
  249. package/dist/sentiment/pipeline.js.map +1 -1
  250. package/dist/sentiment/scorer.js +1 -1
  251. package/dist/sentiment/store.d.ts +1 -1
  252. package/dist/sentiment/store.js +1 -1
  253. package/dist/sentiment/store.js.map +1 -1
  254. package/dist/sentiment/trends.d.ts +1 -1
  255. package/dist/sentiment/trends.js.map +1 -1
  256. package/dist/sentiment/types.js.map +1 -1
  257. package/dist/system-prompt.js +7 -3
  258. package/dist/system-prompt.js.map +1 -1
  259. package/dist/tool-kit.d.ts +7 -7
  260. package/dist/tool-kit.js +4 -4
  261. package/dist/tool-kit.js.map +1 -1
  262. package/dist/tools/fundamentals/company-overview.js +12 -7
  263. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  264. package/dist/tools/fundamentals/comps.js +19 -10
  265. package/dist/tools/fundamentals/comps.js.map +1 -1
  266. package/dist/tools/fundamentals/dcf.js +24 -12
  267. package/dist/tools/fundamentals/dcf.js.map +1 -1
  268. package/dist/tools/fundamentals/earnings.js +9 -4
  269. package/dist/tools/fundamentals/earnings.js.map +1 -1
  270. package/dist/tools/fundamentals/financials.js +9 -4
  271. package/dist/tools/fundamentals/financials.js.map +1 -1
  272. package/dist/tools/fundamentals/sec-filings.d.ts +1 -0
  273. package/dist/tools/fundamentals/sec-filings.js +36 -4
  274. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  275. package/dist/tools/index.d.ts +23 -18
  276. package/dist/tools/index.js +53 -38
  277. package/dist/tools/index.js.map +1 -1
  278. package/dist/tools/interaction/ask-user.js +15 -3
  279. package/dist/tools/interaction/ask-user.js.map +1 -1
  280. package/dist/tools/interaction/twitter-login.js +13 -3
  281. package/dist/tools/interaction/twitter-login.js.map +1 -1
  282. package/dist/tools/macro/fear-greed.js +1 -1
  283. package/dist/tools/macro/fear-greed.js.map +1 -1
  284. package/dist/tools/macro/fred-data.d.ts +1 -1
  285. package/dist/tools/macro/fred-data.js +44 -9
  286. package/dist/tools/macro/fred-data.js.map +1 -1
  287. package/dist/tools/market/crypto-history.js +21 -3
  288. package/dist/tools/market/crypto-history.js.map +1 -1
  289. package/dist/tools/market/crypto-price.js +4 -2
  290. package/dist/tools/market/crypto-price.js.map +1 -1
  291. package/dist/tools/market/screen-stocks.d.ts +18 -0
  292. package/dist/tools/market/screen-stocks.js +252 -0
  293. package/dist/tools/market/screen-stocks.js.map +1 -0
  294. package/dist/tools/market/search-ticker.js +161 -9
  295. package/dist/tools/market/search-ticker.js.map +1 -1
  296. package/dist/tools/market/stock-history.d.ts +2 -2
  297. package/dist/tools/market/stock-history.js +27 -8
  298. package/dist/tools/market/stock-history.js.map +1 -1
  299. package/dist/tools/market/stock-quote.js +6 -4
  300. package/dist/tools/market/stock-quote.js.map +1 -1
  301. package/dist/tools/options/greeks.js +1 -2
  302. package/dist/tools/options/greeks.js.map +1 -1
  303. package/dist/tools/options/option-chain.js +27 -9
  304. package/dist/tools/options/option-chain.js.map +1 -1
  305. package/dist/tools/portfolio/alerts.d.ts +15 -0
  306. package/dist/tools/portfolio/alerts.js +357 -0
  307. package/dist/tools/portfolio/alerts.js.map +1 -0
  308. package/dist/tools/portfolio/correlation.d.ts +1 -1
  309. package/dist/tools/portfolio/correlation.js +34 -14
  310. package/dist/tools/portfolio/correlation.js.map +1 -1
  311. package/dist/tools/portfolio/daily-report.d.ts +8 -0
  312. package/dist/tools/portfolio/daily-report.js +83 -0
  313. package/dist/tools/portfolio/daily-report.js.map +1 -0
  314. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  315. package/dist/tools/portfolio/holdings-overlap.js +112 -0
  316. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  317. package/dist/tools/portfolio/notifications.d.ts +7 -0
  318. package/dist/tools/portfolio/notifications.js +43 -0
  319. package/dist/tools/portfolio/notifications.js.map +1 -0
  320. package/dist/tools/portfolio/predictions.d.ts +12 -6
  321. package/dist/tools/portfolio/predictions.js +338 -88
  322. package/dist/tools/portfolio/predictions.js.map +1 -1
  323. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  324. package/dist/tools/portfolio/risk-analysis.js +46 -7
  325. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  326. package/dist/tools/portfolio/tracker.d.ts +4 -3
  327. package/dist/tools/portfolio/tracker.js +247 -102
  328. package/dist/tools/portfolio/tracker.js.map +1 -1
  329. package/dist/tools/portfolio/watchlist.d.ts +6 -4
  330. package/dist/tools/portfolio/watchlist.js +209 -101
  331. package/dist/tools/portfolio/watchlist.js.map +1 -1
  332. package/dist/tools/sentiment/reddit-sentiment.js +24 -11
  333. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  334. package/dist/tools/sentiment/sentiment-summary.js +71 -14
  335. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  336. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  337. package/dist/tools/sentiment/sentiment-trend.js +12 -2
  338. package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
  339. package/dist/tools/sentiment/twitter-sentiment.js +13 -6
  340. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  341. package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
  342. package/dist/tools/sentiment/untrusted-text.js +17 -0
  343. package/dist/tools/sentiment/untrusted-text.js.map +1 -0
  344. package/dist/tools/sentiment/web-search.js +37 -12
  345. package/dist/tools/sentiment/web-search.js.map +1 -1
  346. package/dist/tools/sentiment/web-sentiment.js +16 -4
  347. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  348. package/dist/tools/technical/backtest.d.ts +3 -3
  349. package/dist/tools/technical/backtest.js +65 -44
  350. package/dist/tools/technical/backtest.js.map +1 -1
  351. package/dist/tools/technical/indicators.js +24 -8
  352. package/dist/tools/technical/indicators.js.map +1 -1
  353. package/dist/types/index.d.ts +3 -3
  354. package/dist/types/index.js.map +1 -1
  355. package/dist/types/market.d.ts +1 -0
  356. package/dist/types/options.d.ts +10 -0
  357. package/dist/types/portfolio.d.ts +41 -4
  358. package/dist/workflows/compare-assets.d.ts +0 -3
  359. package/dist/workflows/compare-assets.js +55 -10
  360. package/dist/workflows/compare-assets.js.map +1 -1
  361. package/dist/workflows/index.d.ts +3 -4
  362. package/dist/workflows/index.js +3 -3
  363. package/dist/workflows/index.js.map +1 -1
  364. package/dist/workflows/options-screener.d.ts +0 -3
  365. package/dist/workflows/options-screener.js +88 -14
  366. package/dist/workflows/options-screener.js.map +1 -1
  367. package/dist/workflows/portfolio-builder.d.ts +0 -3
  368. package/dist/workflows/portfolio-builder.js +7 -11
  369. package/dist/workflows/portfolio-builder.js.map +1 -1
  370. package/gui/server/ask-user-bridge.ts +82 -0
  371. package/gui/server/automation-heartbeat.ts +97 -0
  372. package/gui/server/background-quotes.ts +97 -1
  373. package/gui/server/chat-event-adapter.ts +32 -10
  374. package/gui/server/chat-run-session.ts +16 -0
  375. package/gui/server/gui-session-manager.ts +5 -0
  376. package/gui/server/invoke-tool.ts +144 -1
  377. package/gui/server/live-chat-event-adapter.ts +21 -6
  378. package/gui/server/market-state-api.ts +315 -0
  379. package/gui/server/model-setup.ts +149 -2
  380. package/gui/server/private-api-access.ts +62 -0
  381. package/gui/server/projector.ts +58 -11
  382. package/gui/server/prompt-observation.ts +58 -0
  383. package/gui/server/quote-snapshot-store.ts +50 -0
  384. package/gui/server/server.ts +236 -376
  385. package/gui/server/session-actions.ts +186 -1
  386. package/gui/server/session-entry-wait.ts +81 -0
  387. package/gui/server/shutdown.ts +47 -0
  388. package/gui/server/tool-invoke-ack.ts +49 -0
  389. package/gui/server/tool-metadata.ts +23 -10
  390. package/gui/server/websocket.ts +13 -3
  391. package/gui/server/writer-lock.ts +6 -2
  392. package/gui/server/ws-hub.ts +292 -0
  393. package/gui/shared/chat-events.ts +16 -1
  394. package/gui/shared/event-reducer.ts +24 -6
  395. package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +1 -0
  396. package/gui/web/dist/assets/index-2KZtKBmu.css +1 -0
  397. package/gui/web/dist/assets/index-CveNgtDg.js +69 -0
  398. package/gui/web/dist/index.html +2 -2
  399. package/package.json +22 -12
  400. package/src/analysts/contracts.ts +10 -23
  401. package/src/analysts/orchestrator.ts +8 -43
  402. package/src/cli.ts +37 -13
  403. package/src/config.ts +99 -7
  404. package/src/index.ts +1 -1
  405. package/src/infra/browser.ts +4 -2
  406. package/src/infra/cache.ts +41 -30
  407. package/src/infra/http-client.ts +72 -6
  408. package/src/infra/index.ts +7 -10
  409. package/src/infra/native-dependencies.ts +8 -3
  410. package/src/infra/node-version.ts +3 -1
  411. package/src/infra/opencandle-paths.ts +3 -14
  412. package/src/infra/rate-limiter.ts +32 -20
  413. package/src/market-state/alert-conditions.ts +82 -0
  414. package/src/market-state/alert-runner.ts +863 -0
  415. package/src/market-state/daily-report.ts +247 -0
  416. package/src/market-state/local-automation-service.ts +162 -0
  417. package/src/market-state/notification-delivery.ts +158 -0
  418. package/src/market-state/resolve-for-mutation.ts +24 -0
  419. package/src/market-state/resolve.ts +112 -0
  420. package/src/market-state/service.ts +2344 -0
  421. package/src/memory/index.ts +7 -7
  422. package/src/memory/manager.ts +57 -26
  423. package/src/memory/retrieval.ts +8 -7
  424. package/src/memory/sqlite.ts +407 -6
  425. package/src/memory/storage.ts +8 -17
  426. package/src/memory/tool-defaults.ts +60 -39
  427. package/src/memory/types.ts +7 -3
  428. package/src/monitor.ts +121 -0
  429. package/src/onboarding/connect.ts +10 -33
  430. package/src/onboarding/credential-interceptor.ts +3 -15
  431. package/src/onboarding/degradation-accumulator.ts +1 -3
  432. package/src/onboarding/providers.ts +9 -40
  433. package/src/onboarding/state.ts +4 -15
  434. package/src/onboarding/tool-helpers.ts +2 -9
  435. package/src/onboarding/tool-tags.ts +6 -6
  436. package/src/onboarding/validation.ts +14 -20
  437. package/src/pi/opencandle-extension.ts +795 -120
  438. package/src/pi/session.ts +7 -5
  439. package/src/pi/setup.ts +61 -33
  440. package/src/pi/tool-adapter.ts +5 -2
  441. package/src/prompts/context-builder.ts +143 -21
  442. package/src/prompts/disclaimer.ts +1 -1
  443. package/src/prompts/policy-cards.ts +220 -0
  444. package/src/prompts/sections.ts +4 -4
  445. package/src/prompts/symbol-preflight.ts +80 -0
  446. package/src/prompts/workflow-prompts.ts +231 -28
  447. package/src/providers/alpha-vantage.ts +82 -40
  448. package/src/providers/coingecko.ts +2 -5
  449. package/src/providers/errors.ts +9 -0
  450. package/src/providers/exa-search.ts +24 -22
  451. package/src/providers/fear-greed.ts +1 -1
  452. package/src/providers/finnhub.ts +7 -6
  453. package/src/providers/fred.ts +3 -3
  454. package/src/providers/index.ts +14 -6
  455. package/src/providers/reddit.ts +17 -6
  456. package/src/providers/sec-edgar.ts +235 -5
  457. package/src/providers/tradingview.ts +399 -0
  458. package/src/providers/twitter.ts +6 -8
  459. package/src/providers/web-search.ts +30 -20
  460. package/src/providers/with-fallback.ts +8 -7
  461. package/src/providers/wrap-provider.ts +15 -10
  462. package/src/providers/yahoo-finance.ts +292 -20
  463. package/src/routing/classify-intent.ts +186 -4
  464. package/src/routing/defaults.ts +4 -4
  465. package/src/routing/entity-extractor.ts +428 -28
  466. package/src/routing/fund-symbols.ts +58 -0
  467. package/src/routing/horizon.ts +7 -0
  468. package/src/routing/index.ts +60 -16
  469. package/src/routing/legacy-rule-router.ts +13 -0
  470. package/src/routing/planning.ts +823 -0
  471. package/src/routing/route-manifest.ts +309 -0
  472. package/src/routing/router-llm-client.ts +4 -4
  473. package/src/routing/router-prompt.ts +52 -52
  474. package/src/routing/router-types.ts +18 -0
  475. package/src/routing/router.ts +717 -20
  476. package/src/routing/slot-resolver.ts +75 -14
  477. package/src/routing/symbol-disambiguator.ts +72 -0
  478. package/src/routing/turn-context.ts +108 -0
  479. package/src/routing/types.ts +15 -1
  480. package/src/runtime/answer-contracts.ts +672 -0
  481. package/src/runtime/artifact-contracts.ts +77 -0
  482. package/src/runtime/planning-evidence.ts +682 -0
  483. package/src/runtime/prompt-step.ts +1 -16
  484. package/src/runtime/run-context.ts +12 -2
  485. package/src/runtime/session-coordinator.ts +297 -56
  486. package/src/runtime/session-title.ts +60 -0
  487. package/src/runtime/tool-defaults-wrapper.ts +1 -3
  488. package/src/runtime/validation.ts +1 -4
  489. package/src/runtime/workflow-events.ts +7 -7
  490. package/src/runtime/workflow-runner.ts +5 -11
  491. package/src/sentiment/adapters/finnhub.ts +7 -2
  492. package/src/sentiment/adapters/reddit.ts +2 -2
  493. package/src/sentiment/adapters/twitter.ts +1 -1
  494. package/src/sentiment/adapters/web.ts +1 -1
  495. package/src/sentiment/index.ts +16 -26
  496. package/src/sentiment/keywords.ts +26 -4
  497. package/src/sentiment/pipeline.ts +15 -4
  498. package/src/sentiment/scorer.ts +1 -1
  499. package/src/sentiment/store.ts +2 -2
  500. package/src/sentiment/trends.ts +9 -3
  501. package/src/sentiment/types.ts +5 -4
  502. package/src/system-prompt.ts +7 -3
  503. package/src/tool-kit.ts +10 -9
  504. package/src/tools/fundamentals/company-overview.ts +20 -10
  505. package/src/tools/fundamentals/comps.ts +69 -56
  506. package/src/tools/fundamentals/dcf.ts +146 -96
  507. package/src/tools/fundamentals/earnings.ts +17 -7
  508. package/src/tools/fundamentals/financials.ts +17 -8
  509. package/src/tools/fundamentals/sec-filings.ts +52 -8
  510. package/src/tools/index.ts +53 -38
  511. package/src/tools/interaction/ask-user.ts +22 -10
  512. package/src/tools/interaction/twitter-login.ts +17 -5
  513. package/src/tools/macro/fear-greed.ts +2 -2
  514. package/src/tools/macro/fred-data.ts +80 -42
  515. package/src/tools/market/crypto-history.ts +25 -4
  516. package/src/tools/market/crypto-price.ts +7 -7
  517. package/src/tools/market/screen-stocks.ts +279 -0
  518. package/src/tools/market/search-ticker.ts +219 -18
  519. package/src/tools/market/stock-history.ts +38 -13
  520. package/src/tools/market/stock-quote.ts +11 -8
  521. package/src/tools/options/greeks.ts +5 -6
  522. package/src/tools/options/option-chain.ts +47 -18
  523. package/src/tools/portfolio/alerts.ts +457 -0
  524. package/src/tools/portfolio/correlation.ts +48 -21
  525. package/src/tools/portfolio/daily-report.ts +101 -0
  526. package/src/tools/portfolio/holdings-overlap.ts +139 -0
  527. package/src/tools/portfolio/notifications.ts +45 -0
  528. package/src/tools/portfolio/predictions.ts +407 -107
  529. package/src/tools/portfolio/risk-analysis.ts +47 -8
  530. package/src/tools/portfolio/tracker.ts +271 -110
  531. package/src/tools/portfolio/watchlist.ts +251 -116
  532. package/src/tools/sentiment/reddit-sentiment.ts +51 -25
  533. package/src/tools/sentiment/sentiment-summary.ts +116 -35
  534. package/src/tools/sentiment/sentiment-trend.ts +24 -7
  535. package/src/tools/sentiment/twitter-sentiment.ts +23 -16
  536. package/src/tools/sentiment/untrusted-text.ts +21 -0
  537. package/src/tools/sentiment/web-search.ts +52 -16
  538. package/src/tools/sentiment/web-sentiment.ts +27 -11
  539. package/src/tools/technical/backtest.ts +78 -47
  540. package/src/tools/technical/indicators.ts +40 -17
  541. package/src/types/index.ts +8 -3
  542. package/src/types/market.ts +1 -0
  543. package/src/types/options.ts +17 -0
  544. package/src/types/portfolio.ts +46 -4
  545. package/src/types/sentiment.ts +2 -2
  546. package/src/workflows/compare-assets.ts +67 -19
  547. package/src/workflows/index.ts +3 -4
  548. package/src/workflows/options-screener.ts +98 -22
  549. package/src/workflows/portfolio-builder.ts +40 -29
  550. package/dist/runtime/index.d.ts +0 -16
  551. package/dist/runtime/index.js +0 -10
  552. package/dist/runtime/index.js.map +0 -1
  553. package/dist/runtime/provider-ids.d.ts +0 -14
  554. package/dist/runtime/provider-ids.js +0 -14
  555. package/dist/runtime/provider-ids.js.map +0 -1
  556. package/dist/workflows/types.d.ts +0 -4
  557. package/dist/workflows/types.js +0 -2
  558. package/dist/workflows/types.js.map +0 -1
  559. package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +0 -1
  560. package/gui/web/dist/assets/index-DBrWq43L.css +0 -1
  561. package/gui/web/dist/assets/index-RflHaj0y.js +0 -67
  562. package/src/runtime/index.ts +0 -55
  563. package/src/runtime/provider-ids.ts +0 -15
  564. package/src/workflows/types.ts +0 -4
@@ -1,12 +1,23 @@
1
- import { httpGet } from "../infra/http-client.js";
2
- import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
3
- import { rateLimiter } from "../infra/rate-limiter.js";
4
1
  import { StealthBrowser } from "../infra/browser.js";
5
- import type { StockQuote, OHLCV } from "../types/market.js";
6
- import type { OptionsChain, OptionContract } from "../types/options.js";
2
+ import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
3
+ import { HttpError, httpGet } from "../infra/http-client.js";
4
+ import { rateLimiter } from "../infra/rate-limiter.js";
7
5
  import { computeGreeks } from "../tools/options/greeks.js";
6
+ import type { OHLCV, StockQuote } from "../types/market.js";
7
+ import type {
8
+ OptionContract,
9
+ OptionsChain,
10
+ OptionsMarketSession,
11
+ OptionsQuoteStatus,
12
+ } from "../types/options.js";
13
+ import type { FundHoldings } from "../types/portfolio.js";
14
+ import { InvalidSymbolError } from "./errors.js";
8
15
 
9
16
  const BASE_URL = "https://query1.finance.yahoo.com/v8/finance/chart";
17
+ const QUOTE_SUMMARY_URL = "https://query1.finance.yahoo.com/v10/finance/quoteSummary";
18
+ const STALE_QUOTE_MAX_RETRY_AFTER_MS = 1_000;
19
+
20
+ type YahooNumber = number | { raw?: number; fmt?: string };
10
21
 
11
22
  interface YahooChartResponse {
12
23
  chart: {
@@ -28,6 +39,29 @@ interface YahooChartResponse {
28
39
  };
29
40
  }
30
41
 
42
+ interface YahooQuoteSummaryResponse {
43
+ quoteSummary: {
44
+ result?: Array<{
45
+ price?: {
46
+ symbol?: string;
47
+ shortName?: string;
48
+ longName?: string;
49
+ };
50
+ topHoldings?: {
51
+ holdings?: Array<{
52
+ symbol?: string;
53
+ holdingName?: string;
54
+ holdingPercent?: YahooNumber;
55
+ }>;
56
+ equityHoldings?: {
57
+ sectorWeightings?: Array<Record<string, YahooNumber>>;
58
+ };
59
+ };
60
+ }>;
61
+ error?: { code?: string; description?: string } | null;
62
+ };
63
+ }
64
+
31
65
  export async function getQuote(symbol: string): Promise<StockQuote> {
32
66
  const cacheKey = `yahoo:quote:${symbol}`;
33
67
  const cached = cache.get<StockQuote>(cacheKey);
@@ -39,6 +73,7 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
39
73
  const url = `${BASE_URL}/${encodeURIComponent(symbol)}?interval=1d&range=1d`;
40
74
  const data = await httpGet<YahooChartResponse>(url, {
41
75
  headers: { "User-Agent": "OpenCandle/1.0" },
76
+ maxRetryAfterMs: STALE_QUOTE_MAX_RETRY_AFTER_MS,
42
77
  });
43
78
 
44
79
  if (data.chart.error) {
@@ -72,8 +107,16 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
72
107
  week52High: meta.fiftyTwoWeekHigh ?? 0,
73
108
  week52Low: meta.fiftyTwoWeekLow ?? 0,
74
109
  timestamp: Date.now(),
110
+ currency:
111
+ typeof meta.currency === "string" && meta.currency.trim() !== ""
112
+ ? meta.currency.trim().toUpperCase()
113
+ : null,
75
114
  };
76
115
 
116
+ if (isZeroResultQuote(quote)) {
117
+ throw new InvalidSymbolError(symbol.toUpperCase(), "yahoo");
118
+ }
119
+
77
120
  cache.set(cacheKey, quote, TTL.QUOTE);
78
121
  return quote;
79
122
  } catch (error) {
@@ -83,6 +126,16 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
83
126
  }
84
127
  }
85
128
 
129
+ function isZeroResultQuote(quote: StockQuote): boolean {
130
+ return (
131
+ quote.price === 0 &&
132
+ quote.volume === 0 &&
133
+ quote.week52High === 0 &&
134
+ quote.week52Low === 0 &&
135
+ quote.marketCap === 0
136
+ );
137
+ }
138
+
86
139
  export async function getHistory(
87
140
  symbol: string,
88
141
  range: string = "6mo",
@@ -128,10 +181,128 @@ export async function getHistory(
128
181
  }
129
182
  }
130
183
 
184
+ export async function getFundHoldings(symbol: string): Promise<FundHoldings> {
185
+ const normalizedSymbol = symbol.toUpperCase();
186
+ const cacheKey = `yahoo:fund-holdings:${normalizedSymbol}`;
187
+ const cached = cache.get<FundHoldings>(cacheKey);
188
+ if (cached) return cached;
189
+
190
+ try {
191
+ await rateLimiter.acquire("yahoo");
192
+
193
+ const data = await getFundHoldingsSummary(normalizedSymbol);
194
+ const result = data.quoteSummary.result?.[0];
195
+ if (data.quoteSummary.error) {
196
+ throw new Error(
197
+ `Yahoo Finance: ${data.quoteSummary.error.description ?? data.quoteSummary.error.code ?? "quoteSummary error"}`,
198
+ );
199
+ }
200
+ if (!result?.topHoldings?.holdings?.length) {
201
+ throw new Error(`Yahoo Finance: no fund holdings returned for ${normalizedSymbol}`);
202
+ }
203
+
204
+ const holdings: FundHoldings = {
205
+ symbol: result.price?.symbol?.toUpperCase() ?? normalizedSymbol,
206
+ name: result.price?.shortName ?? result.price?.longName,
207
+ provider: "yahoo",
208
+ holdings: result.topHoldings.holdings.flatMap((holding) => {
209
+ const holdingSymbol = holding.symbol?.trim().toUpperCase();
210
+ const weight = normalizeHoldingWeight(holding.holdingPercent);
211
+ if (!holdingSymbol || weight === undefined) return [];
212
+ return [
213
+ {
214
+ symbol: holdingSymbol,
215
+ name: holding.holdingName?.trim() || holdingSymbol,
216
+ weight,
217
+ },
218
+ ];
219
+ }),
220
+ sectorWeights: normalizeSectorWeights(result.topHoldings.equityHoldings?.sectorWeightings),
221
+ };
222
+ if (holdings.holdings.length === 0) {
223
+ throw new Error(`Yahoo Finance: no weighted fund holdings returned for ${normalizedSymbol}`);
224
+ }
225
+
226
+ cache.set(cacheKey, holdings, TTL.FUNDAMENTALS);
227
+ return holdings;
228
+ } catch (error) {
229
+ const stale = cache.getStale<FundHoldings>(cacheKey, STALE_LIMIT.FUNDAMENTALS);
230
+ if (stale) return stale.value;
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ async function getFundHoldingsSummary(symbol: string): Promise<YahooQuoteSummaryResponse> {
236
+ try {
237
+ return await fetchFundHoldingsSummary(symbol);
238
+ } catch (error) {
239
+ if (!isYahooAuthError(error)) throw error;
240
+ return fetchFundHoldingsSummaryWithCrumb(symbol);
241
+ }
242
+ }
243
+
244
+ async function fetchFundHoldingsSummary(symbol: string): Promise<YahooQuoteSummaryResponse> {
245
+ const modules = encodeURIComponent("price,topHoldings");
246
+ const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}`;
247
+ return httpGet<YahooQuoteSummaryResponse>(url, {
248
+ headers: { "User-Agent": "OpenCandle/1.0" },
249
+ });
250
+ }
251
+
252
+ async function fetchFundHoldingsSummaryWithCrumb(
253
+ symbol: string,
254
+ ): Promise<YahooQuoteSummaryResponse> {
255
+ const modules = encodeURIComponent("price,topHoldings");
256
+ const { crumb, cookie } = await getYahooCrumb();
257
+ const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}&crumb=${encodeURIComponent(crumb)}`;
258
+ try {
259
+ return await httpGet<YahooQuoteSummaryResponse>(url, {
260
+ headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
261
+ });
262
+ } catch (error) {
263
+ if (!isYahooAuthError(error)) throw error;
264
+ clearCrumbCache();
265
+ const fresh = await getYahooCrumb();
266
+ const retryUrl = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}&crumb=${encodeURIComponent(fresh.crumb)}`;
267
+ return httpGet<YahooQuoteSummaryResponse>(retryUrl, {
268
+ headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
269
+ });
270
+ }
271
+ }
272
+
273
+ function isYahooAuthError(error: unknown): boolean {
274
+ return error instanceof HttpError && (error.status === 401 || error.status === 429);
275
+ }
276
+
277
+ function normalizeHoldingWeight(value: YahooNumber | undefined): number | undefined {
278
+ const numeric = typeof value === "number" ? value : value?.raw;
279
+ if (numeric === undefined || !Number.isFinite(numeric) || numeric <= 0) return undefined;
280
+ return numeric > 1 ? roundWeight(numeric / 100) : roundWeight(numeric);
281
+ }
282
+
283
+ function normalizeSectorWeights(
284
+ sectors: Array<Record<string, YahooNumber>> | undefined,
285
+ ): Record<string, number> | undefined {
286
+ if (!sectors?.length) return undefined;
287
+ const weights: Record<string, number> = {};
288
+ for (const sector of sectors) {
289
+ for (const [name, rawWeight] of Object.entries(sector)) {
290
+ const weight = normalizeHoldingWeight(rawWeight);
291
+ if (weight !== undefined) weights[name] = weight;
292
+ }
293
+ }
294
+ return Object.keys(weights).length > 0 ? weights : undefined;
295
+ }
296
+
297
+ function roundWeight(value: number): number {
298
+ return Math.round(value * 10_000) / 10_000;
299
+ }
300
+
131
301
  // --- Options Chain (v7 API with crumb+cookie auth) ---
132
302
 
133
303
  const BROWSER_UA =
134
304
  "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";
305
+ const YAHOO_RAW_FETCH_TIMEOUT_MS = 10_000;
135
306
 
136
307
  let cachedCrumb: { crumb: string; cookie: string; expiresAt: number } | null = null;
137
308
 
@@ -147,15 +318,30 @@ export async function getYahooCrumb(): Promise<{ crumb: string; cookie: string }
147
318
  // Step 1: Hit fc.yahoo.com to get a session cookie
148
319
  const cookieRes = await fetch("https://fc.yahoo.com/t", {
149
320
  headers: { "User-Agent": BROWSER_UA },
321
+ signal: yahooRawFetchSignal(),
150
322
  });
151
323
  const setCookie = cookieRes.headers.get("set-cookie") ?? "";
152
324
  const cookie = setCookie.split(";")[0]; // Extract just the cookie value
325
+ if (!cookie) {
326
+ if (!cookieRes.ok) {
327
+ throw new Error(
328
+ `Yahoo crumb cookie request failed: HTTP ${cookieRes.status} ${cookieRes.statusText}`.trim(),
329
+ );
330
+ }
331
+ throw new Error("Yahoo crumb cookie request did not return a session cookie");
332
+ }
153
333
 
154
334
  // Step 2: Use the cookie to get a crumb
155
335
  const crumbRes = await fetch("https://query2.finance.yahoo.com/v1/test/getcrumb", {
156
336
  headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
337
+ signal: yahooRawFetchSignal(),
157
338
  });
158
- const crumb = await crumbRes.text();
339
+ if (!crumbRes.ok) {
340
+ throw new Error(
341
+ `Yahoo crumb request failed: HTTP ${crumbRes.status} ${crumbRes.statusText}`.trim(),
342
+ );
343
+ }
344
+ const crumb = (await crumbRes.text()).trim();
159
345
 
160
346
  if (!crumb || crumb.includes("Unauthorized")) {
161
347
  throw new Error("Failed to acquire Yahoo Finance crumb");
@@ -182,25 +368,23 @@ interface YahooOptionsResponse {
182
368
  };
183
369
  }
184
370
 
185
- export async function getOptionsChain(
186
- symbol: string,
187
- expiration?: number,
188
- ): Promise<OptionsChain> {
371
+ export async function getOptionsChain(symbol: string, expiration?: number): Promise<OptionsChain> {
189
372
  const cacheKey = `yahoo:options:${symbol}:${expiration ?? "nearest"}`;
190
373
  const cached = cache.get<OptionsChain>(cacheKey);
191
374
  if (cached) return cached;
192
375
 
193
376
  await rateLimiter.acquire("yahoo");
194
377
 
195
- const { crumb, cookie } = await getYahooCrumb();
196
378
  const dateParam = expiration ? `&date=${expiration}` : "";
197
- const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
198
379
 
199
380
  let res: Response | null = null;
200
381
  let fetchError: unknown;
201
382
  try {
383
+ const { crumb, cookie } = await getYahooCrumb();
384
+ const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
202
385
  res = await fetch(url, {
203
386
  headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
387
+ signal: yahooRawFetchSignal(),
204
388
  });
205
389
  } catch (error) {
206
390
  fetchError = error;
@@ -214,6 +398,7 @@ export async function getOptionsChain(
214
398
  const retryUrl = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(fresh.crumb)}${dateParam}`;
215
399
  res = await fetch(retryUrl, {
216
400
  headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
401
+ signal: yahooRawFetchSignal(),
217
402
  });
218
403
  } catch (error) {
219
404
  fetchError = error;
@@ -227,7 +412,7 @@ export async function getOptionsChain(
227
412
  try {
228
413
  const browserData = await fetchOptionsViaBrowser(symbol, expiration);
229
414
  if (browserData) {
230
- const chain = parseOptionsResponse(symbol, browserData);
415
+ const chain = parseOptionsResponse(browserData);
231
416
  cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
232
417
  return chain;
233
418
  }
@@ -245,18 +430,25 @@ export async function getOptionsChain(
245
430
  throw new Error(message);
246
431
  }
247
432
  if (browserError instanceof Error) {
248
- const message = fetchError instanceof Error ? fetchError.message : "Yahoo Finance options: fetch failed";
433
+ const message =
434
+ fetchError instanceof Error ? fetchError.message : "Yahoo Finance options: fetch failed";
249
435
  throw new Error(`${message}; browser fallback failed: ${browserError.message}`);
250
436
  }
251
- throw fetchError instanceof Error ? fetchError : new Error("Yahoo Finance options: fetch failed");
437
+ throw fetchError instanceof Error
438
+ ? fetchError
439
+ : new Error("Yahoo Finance options: fetch failed");
252
440
  }
253
441
 
254
442
  const data: YahooOptionsResponse = await res.json();
255
- const chain = parseOptionsResponse(symbol, data);
443
+ const chain = parseOptionsResponse(data);
256
444
  cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
257
445
  return chain;
258
446
  }
259
447
 
448
+ function yahooRawFetchSignal(): AbortSignal {
449
+ return AbortSignal.timeout(YAHOO_RAW_FETCH_TIMEOUT_MS);
450
+ }
451
+
260
452
  /**
261
453
  * Compute time to expiry in years from a Yahoo expiration timestamp (midnight UTC).
262
454
  * US equity options expire at 4:00 PM ET. During EDT that is 20:00 UTC.
@@ -265,7 +457,7 @@ export async function getOptionsChain(
265
457
  */
266
458
  export function computeTimeToExpiry(expirationTs: number, nowMs: number = Date.now()): number {
267
459
  const MARKET_CLOSE_OFFSET_S = 21 * 3600; // 21:00 UTC ≈ 4 PM ET
268
- const MIN_TIME_YEARS = 1 / (365 * 24); // ~1 hour floor
460
+ const MIN_TIME_YEARS = 1 / (365 * 24); // ~1 hour floor
269
461
  const SECONDS_PER_YEAR = 365 * 24 * 3600;
270
462
 
271
463
  const expiryCloseTs = expirationTs + MARKET_CLOSE_OFFSET_S;
@@ -275,7 +467,73 @@ export function computeTimeToExpiry(expirationTs: number, nowMs: number = Date.n
275
467
  return Math.max(MIN_TIME_YEARS, remainingS / SECONDS_PER_YEAR);
276
468
  }
277
469
 
278
- function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): OptionsChain {
470
+ function getUsOptionsMarketSession(now: Date = new Date()): OptionsMarketSession {
471
+ const parts = new Intl.DateTimeFormat("en-US", {
472
+ timeZone: "America/New_York",
473
+ weekday: "short",
474
+ hour: "2-digit",
475
+ minute: "2-digit",
476
+ hour12: false,
477
+ }).formatToParts(now);
478
+ const part = (type: string): string => parts.find((p) => p.type === type)?.value ?? "";
479
+ const weekday = part("weekday");
480
+ if (weekday === "Sat" || weekday === "Sun") return "closed";
481
+
482
+ const hour = Number(part("hour"));
483
+ const minute = Number(part("minute"));
484
+ const minutes = hour * 60 + minute;
485
+ if (minutes < 9 * 60 + 30) return "pre_market";
486
+ if (minutes < 16 * 60) return "regular";
487
+ return "after_hours";
488
+ }
489
+
490
+ function buildOptionsQuoteStatus(
491
+ contracts: OptionContract[],
492
+ now: Date = new Date(),
493
+ ): OptionsQuoteStatus {
494
+ const marketSession = getUsOptionsMarketSession(now);
495
+ const totalContracts = contracts.length;
496
+ const zeroBidAskContracts = contracts.filter((c) => c.bid === 0 && c.ask === 0).length;
497
+ const allZeroBidAsk = totalContracts > 0 && zeroBidAskContracts === totalContracts;
498
+ const hasLiveBidAsk = contracts.some((c) => c.bid > 0 || c.ask > 0);
499
+
500
+ if (allZeroBidAsk && marketSession !== "regular") {
501
+ return {
502
+ marketSession,
503
+ bidAskState: "closed_market_or_stale_quotes",
504
+ zeroBidAskContracts,
505
+ totalContracts,
506
+ warning:
507
+ "All option contracts have $0.00/$0.00 bid/ask before regular options trading or outside market hours; treat bid/ask as closed-market or stale until the market opens.",
508
+ };
509
+ }
510
+
511
+ if (allZeroBidAsk) {
512
+ return {
513
+ marketSession,
514
+ bidAskState: "live_zero_bid_ask",
515
+ zeroBidAskContracts,
516
+ totalContracts,
517
+ warning:
518
+ "All option contracts have $0.00/$0.00 bid/ask during regular options trading hours; verify with a broker, but this may indicate live illiquidity.",
519
+ };
520
+ }
521
+
522
+ return {
523
+ marketSession,
524
+ bidAskState: hasLiveBidAsk ? "live_quotes" : "mixed_or_unknown",
525
+ zeroBidAskContracts,
526
+ totalContracts,
527
+ ...(marketSession !== "regular"
528
+ ? {
529
+ warning:
530
+ "Options bid/ask quotes may be stale outside regular options trading hours; verify live executable prices after the market opens.",
531
+ }
532
+ : {}),
533
+ };
534
+ }
535
+
536
+ function parseOptionsResponse(data: YahooOptionsResponse): OptionsChain {
279
537
  if (data.optionChain.error) {
280
538
  throw new Error(`Yahoo Finance options: ${JSON.stringify(data.optionChain.error)}`);
281
539
  }
@@ -284,6 +542,9 @@ function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): Optio
284
542
  const quote = result.quote;
285
543
  const underlyingPrice = quote.regularMarketPrice ?? 0;
286
544
  const opts = result.options[0];
545
+ if (!opts && underlyingPrice === 0) {
546
+ throw new InvalidSymbolError(result.underlyingSymbol, "yahoo");
547
+ }
287
548
  const riskFreeRate = 0.05;
288
549
 
289
550
  const expirationTs = opts.expirationDate;
@@ -293,7 +554,14 @@ function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): Optio
293
554
  const mapContract = (c: any, type: "call" | "put"): OptionContract => {
294
555
  const strike = c.strike ?? c.strike?.raw ?? 0;
295
556
  const iv = c.impliedVolatility ?? c.impliedVolatility?.raw ?? 0;
296
- const greeks = computeGreeks({ type, spot: underlyingPrice, strike, timeYears, iv, riskFreeRate });
557
+ const greeks = computeGreeks({
558
+ type,
559
+ spot: underlyingPrice,
560
+ strike,
561
+ timeYears,
562
+ iv,
563
+ riskFreeRate,
564
+ });
297
565
  return {
298
566
  contractSymbol: c.contractSymbol ?? "",
299
567
  type,
@@ -314,17 +582,21 @@ function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): Optio
314
582
  const puts = (opts.puts ?? []).map((c: any) => mapContract(c, "put"));
315
583
  const totalCallVolume = calls.reduce((s, c) => s + c.volume, 0);
316
584
  const totalPutVolume = puts.reduce((s, c) => s + c.volume, 0);
585
+ const quoteStatus = buildOptionsQuoteStatus([...calls, ...puts]);
317
586
 
318
587
  return {
319
588
  symbol: result.underlyingSymbol,
320
589
  underlyingPrice,
321
590
  expirationDate,
322
- expirationDates: result.expirationDates.map((ts) => new Date(ts * 1000).toISOString().split("T")[0]),
591
+ expirationDates: result.expirationDates.map(
592
+ (ts) => new Date(ts * 1000).toISOString().split("T")[0],
593
+ ),
323
594
  calls,
324
595
  puts,
325
596
  totalCallVolume,
326
597
  totalPutVolume,
327
598
  putCallRatio: totalCallVolume > 0 ? totalPutVolume / totalCallVolume : 0,
599
+ quoteStatus,
328
600
  fetchedAt: new Date().toISOString(),
329
601
  };
330
602
  }
@@ -1,5 +1,5 @@
1
- import type { ClassificationResult, WorkflowType, ExtractedEntities } from "./types.js";
2
1
  import { extractEntities } from "./entity-extractor.js";
2
+ import type { ClassificationResult, ExtractedEntities, WorkflowType } from "./types.js";
3
3
 
4
4
  interface Rule {
5
5
  workflow: WorkflowType;
@@ -29,7 +29,23 @@ const RULES: Rule[] = [
29
29
  entities.symbols.length === 1 &&
30
30
  (/\bis\s+\S+\s+(?:attractive|undervalued|overvalued|cheap|expensive)/i.test(lower) ||
31
31
  /\bshould\s+i\s+buy\s+\$?[a-z]{1,5}\b/i.test(lower) ||
32
- /\bwhat\s+do\s+you\s+think\s+(?:of|about)\s+\$?[a-z]{1,5}\b/i.test(lower))
32
+ /\bwhat\s+do\s+you\s+think\s+(?:of|about)\s+\$?[a-z]{1,5}\b/i.test(lower) ||
33
+ /\bbull\s+(?:and|or)\s+bear\s+case\b/i.test(lower))
34
+ );
35
+ },
36
+ },
37
+ // Portfolio risk for existing holdings must route before multi-symbol compare.
38
+ {
39
+ workflow: "watchlist_or_tracking",
40
+ confidence: 0.9,
41
+ test: (input, entities) => {
42
+ const lower = input.toLowerCase();
43
+ return (
44
+ entities.symbols.length >= 1 &&
45
+ (/\bi\s+own\b/.test(lower) || /\bmy\s+holdings\b/.test(lower)) &&
46
+ (/\bportfolio\s+risk\b/.test(lower) ||
47
+ /\bbiggest\s+risk\b/.test(lower) ||
48
+ /\bconcentration\b/.test(lower))
33
49
  );
34
50
  },
35
51
  },
@@ -56,6 +72,65 @@ const RULES: Rule[] = [
56
72
  return hasNewsKeyword;
57
73
  },
58
74
  },
75
+ // Tool-backed finance tasks that are not a structured multi-step workflow.
76
+ {
77
+ workflow: "general_finance_qa",
78
+ confidence: 0.9,
79
+ test: (input, entities) => {
80
+ const lower = input.toLowerCase();
81
+ const hasOptionKeywords =
82
+ /\bcalls?\b/.test(lower) ||
83
+ /\bputs?\b/.test(lower) ||
84
+ /\boption(?:s)?\s*chain\b/.test(lower) ||
85
+ /\boptions?\b/.test(lower);
86
+ const hasCompareKeywords =
87
+ /\bcompare\b/.test(lower) ||
88
+ /\bvs\.?\b/.test(lower) ||
89
+ /\bversus\b/.test(lower) ||
90
+ /\bwhich\s+is\s+better\b/.test(lower);
91
+
92
+ if (hasOptionKeywords && entities.symbols.length >= 1) return false;
93
+ if (hasCompareKeywords && entities.symbols.length >= 2) return false;
94
+
95
+ return (
96
+ /\bbacktest\b/.test(lower) || /\bsentiment\b/.test(lower) || /\brate\s+cuts?\b/.test(lower)
97
+ );
98
+ },
99
+ },
100
+ // Broad market / sector / macro research that should receive the general
101
+ // analyst fallback rather than disappearing into an unclassified turn.
102
+ {
103
+ workflow: "general_finance_qa",
104
+ confidence: 0.85,
105
+ test: (input) => {
106
+ const lower = input.toLowerCase();
107
+ const hasResearchVerb =
108
+ /\banaly[sz]e\b/.test(lower) ||
109
+ /\bevaluat(?:e|ion)\b/.test(lower) ||
110
+ /\breview\b/.test(lower) ||
111
+ /\bdiscuss\b/.test(lower) ||
112
+ /\bpredict\b/.test(lower) ||
113
+ /\bassess\b/.test(lower) ||
114
+ /^what\b/.test(lower);
115
+ const hasBroadFinanceTopic =
116
+ /\bmarket\s+structure\b/.test(lower) ||
117
+ /\b(?:sector|industry)\b/.test(lower) ||
118
+ /\bmacro\s+risks?\b/.test(lower) ||
119
+ /\bmonetary\s+policy\b/.test(lower) ||
120
+ /\bemerging\s+markets?\b/.test(lower) ||
121
+ /\bcapital\s+flows?\b/.test(lower) ||
122
+ /\bcurrency\s+fluctuations?\b/.test(lower) ||
123
+ /\binflation\b/.test(lower);
124
+ return hasResearchVerb && hasBroadFinanceTopic;
125
+ },
126
+ },
127
+ // Existing allocation / portfolio review. This is not portfolio construction
128
+ // and should not require a budget.
129
+ {
130
+ workflow: "general_finance_qa",
131
+ confidence: 0.85,
132
+ test: (input) => isPortfolioEvaluationRequest(input),
133
+ },
59
134
  // Options: symbol + option keyword
60
135
  {
61
136
  workflow: "options_screener",
@@ -70,6 +145,14 @@ const RULES: Rule[] = [
70
145
  return hasOptionKeywords && entities.symbols.length >= 1;
71
146
  },
72
147
  },
148
+ // Stateful portfolio/watchlist/alert/prediction mutations must not be
149
+ // mistaken for compare or portfolio-construction workflows just because a
150
+ // cost basis, target, or currency token is present.
151
+ {
152
+ workflow: "watchlist_or_tracking",
153
+ confidence: 0.95,
154
+ test: (input) => isStatefulTrackingRequest(input),
155
+ },
73
156
  // Compare: keyword + 2+ symbols (uppercase)
74
157
  {
75
158
  workflow: "compare_assets",
@@ -88,9 +171,12 @@ const RULES: Rule[] = [
88
171
  {
89
172
  workflow: "compare_assets",
90
173
  confidence: 0.85,
91
- test: (input) => {
174
+ test: (input, entities) => {
92
175
  const lower = input.toLowerCase();
93
- return /\bcompare\s+[a-z]{1,5}(?:\s*,?\s*(?:and\s+)?[a-z]{1,5})+/.test(lower);
176
+ return (
177
+ entities.symbols.length >= 2 &&
178
+ /\bcompare\s+[a-z]{1,5}\b(?:\s*,?\s*(?:and\s+)?[a-z]{1,5}\b)+/.test(lower)
179
+ );
94
180
  },
95
181
  },
96
182
  // Compare: 2+ uppercase symbols without explicit keyword
@@ -161,6 +247,9 @@ const RULES: Rule[] = [
161
247
  },
162
248
  ];
163
249
 
250
+ /**
251
+ * @deprecated Use the LLM router (`route`) for new classification paths; keep this only for rules-mode fallback and deterministic safety nets.
252
+ */
164
253
  export function classifyIntent(input: string): ClassificationResult {
165
254
  const trimmed = input.trim();
166
255
  if (!trimmed) {
@@ -192,3 +281,96 @@ export function classifyIntent(input: string): ClassificationResult {
192
281
  entities,
193
282
  };
194
283
  }
284
+
285
+ function isPortfolioEvaluationRequest(input: string): boolean {
286
+ const lower = input.toLowerCase();
287
+ const hasEvaluationIntent =
288
+ /\b(?:evaluat(?:e|ion)|review|assess|analy[sz]e|prospects?|risks?|opportunities?|mitigat(?:e|ion)|adjustment)\b/.test(
289
+ lower,
290
+ );
291
+ const hasPortfolioObject =
292
+ /\b(?:portfolio|allocation|asset\s+allocation|60\/40|equity|fixed\s+income|bonds?)\b/.test(
293
+ lower,
294
+ );
295
+ const hasConstructionIntent =
296
+ /\b(?:build|create|construct|put\s+together|invest|allocate)\b/.test(lower) &&
297
+ /\$\s*\d|\b\d+(?:\.\d+)?\s*k\b|\bbudget\b|\bcapital\b/.test(lower);
298
+ return hasEvaluationIntent && hasPortfolioObject && !hasConstructionIntent;
299
+ }
300
+
301
+ function isStatefulTrackingRequest(input: string): boolean {
302
+ const lower = input.toLowerCase();
303
+ const hasPortfolioConstructionIntent =
304
+ /\b(?:build|create|construct|put\s+together)\b/.test(lower) &&
305
+ /\bportfolio\b/.test(lower) &&
306
+ /\$\s*\d|\b\d+(?:\.\d+)?\s*k\b|\bbudget\b|\bcapital\b/.test(lower);
307
+ const hasStateVerb =
308
+ /\b(?:add|remove|update|record|track|create|configure|check|show|list|view|cancel)\b/.test(
309
+ lower,
310
+ );
311
+ const hasStateObject =
312
+ /\b(?:watchlist|portfolio|holding|holdings|position|positions|prediction|predictions|alert|alerts|daily\s+report|watchlist\s+report|report\s+history)\b/.test(
313
+ lower,
314
+ );
315
+ const hasPortfolioLotShape =
316
+ /\b(?:add|record|track)\b/.test(lower) &&
317
+ /\b\d+(?:\.\d+)?\s+shares?\b/.test(lower) &&
318
+ /\b(?:portfolio|holding|holdings|position|positions)\b/.test(lower);
319
+ if (hasPortfolioConstructionIntent) return false;
320
+ return (hasStateVerb && hasStateObject) || hasPortfolioLotShape;
321
+ }
322
+
323
+ const FINANCE_SIGNAL_TERMS = [
324
+ "stock",
325
+ "stocks",
326
+ "shares",
327
+ "ticker",
328
+ "tickers",
329
+ "etf",
330
+ "etfs",
331
+ "ipo",
332
+ "earnings",
333
+ "dividend",
334
+ "dividends",
335
+ "valuation",
336
+ "stock market",
337
+ "invest",
338
+ "investing",
339
+ "investment",
340
+ "portfolio",
341
+ "watchlist",
342
+ "bond",
343
+ "bonds",
344
+ "bond yield",
345
+ "treasury",
346
+ "the fed",
347
+ "inflation",
348
+ "interest rates",
349
+ "crypto",
350
+ "bitcoin",
351
+ "ethereum",
352
+ "options chain",
353
+ "covered call",
354
+ "puts",
355
+ "bullish",
356
+ "bearish",
357
+ "hedge",
358
+ "price target",
359
+ "cost basis",
360
+ "nasdaq",
361
+ "s&p",
362
+ ];
363
+
364
+ const FINANCE_SIGNAL_PATTERN = new RegExp(
365
+ `\\b(?:${FINANCE_SIGNAL_TERMS.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`,
366
+ "i",
367
+ );
368
+
369
+ /**
370
+ * Deterministic finance-vocabulary check for rules-mode fallback turns whose
371
+ * intent did not match a workflow and whose entities carry no symbols (for
372
+ * example theme prompts about private companies or sectors).
373
+ */
374
+ export function hasFinanceSignals(input: string): boolean {
375
+ return FINANCE_SIGNAL_PATTERN.test(input);
376
+ }